Repository: INCATools/kgcl-rdflib (language: python)

Issue:
merge kgcl_tool into kgcl
merge kgcl_tool into kgcl

Organisation:

* kgcl
     * utils
     * model - autogenerated from linkml
          * kgcl
          * kgmodel
     * apply
     * diff
* tests
    * test_model.py
    * test_apply/
    * test_diff/

CLI:

Follow https://clig.dev/, e.g. `-o` for outputs

For summaries:

![image](https://user-images.githubusercontent.com/50745/133128806-c32e41ef-8065-455b-b00d-6e30a33270b7.png)

ideally this would follow the datamodel, which is hopefully used underneath


Discussion / hints:

Fix patch:
diff --git a/app.py b/app.py
new file mode 100644
index 00000000..9d55c78e
--- /dev/null
+++ b/app.py
@@ -0,0 +1,305 @@
+from flask import (
+    Flask,
+    render_template,
+    request,
+)
+from kgcl.model.kgcl import (
+    NodeRename,
+    NodeObsoletion,
+    NodeUnobsoletion,
+    NodeDeletion,
+    NodeMove,
+    NodeDeepening,
+    NodeShallowing,
+    EdgeCreation,
+    EdgeDeletion,
+    PredicateChange,
+    NodeCreation,
+    ClassCreation,
+    NewSynonym,
+    RemovedNodeFromSubset,
+)
+
+import os
+from kgcl.apply.graph_transformer import apply_patch
+
+# from kgcl.diff.example_kgcl_operations import generate_diff
+import rdflib
+from kgcl.grammar.parser import parse
+
+import kgcl.diff.diff_2_kgcl_single as single
+from kgcl.diff.diff_2_kgcl_single import SingleTripleChangeSummary
+import kgcl.diff.diff_2_kgcl_existential as existential
+from kgcl.diff.diff_2_kgcl_existential import ExistentialChangeSummary
+import kgcl.diff.diff_2_kgcl_triple_annotation as annotation
+from kgcl.diff.diff_2_kgcl_triple_annotation import TripleAnnotationChangeSummary
+
+from kgcl.diff.pretty_print_kgcl import render_instances
+
+# from diff.example_kgcl_operations import generate_diff
+from kgcl.render_kgcl import render
+
+
+def generate_diff():
+    pass
+
+
+app = Flask(__name__)
+
+
+# we could store changes in a data base - do we want to
+@app.route("/", methods=["POST", "GET"])
+def index():
+    examples = [
+        "rename",
+        "nodeCreation",
+        "nodeDeletionByLabel",
+        "nodeDeletionById",
+        "obsoleteByLabel",
+        "obsoleteById",
+        "unobsoleteById",
+        "nodeDeepening",
+        "nodeShallowing",
+        "move",
+        "changeRelationship",
+        "createSynonym",
+        "edgeCreation",
+        "edgeDeletion",
+        "addSubsumptionAxiom",
+        "deleteSubsumptionAxiom",
+    ]
+
+    if request.method == "POST":
+
+        if "apply_changes" in request.form:
+
+            # get input graph from form
+            graph = request.form["graph"]
+
+            # get input kgcl statements from form
+            kgcl = request.form["kgcl"]
+
+            kgcl_transformation(graph, kgcl)
+
+        if "load_example" in request.form:
+            select = request.form.get("comp_select")
+            example = str(select)
+
+            f = open("examples/kgcl/" + example + "/graph.nt", "r")
+            graph = f.read()
+            f.close()
+
+            f = open("examples/kgcl/" + example + "/kgcl", "r")
+            kgcl = f.read()
+            f.close()
+
+            kgcl_transformation(graph, kgcl)
+
+        # parse KGCL input
+        kgcl_model = parse(kgcl)
+
+        # prepare parsed
+        kgcl_model_rendering = ""
+        for o in kgcl_model:
+            kgcl_model_rendering += render(o) + "\n"
+
+        # load transformed graph
+        f = open("examples/kgcl/tmp/transformation.nt", "r")
+        transformation = f.read()
+        f.close()
+
+        return render_template(
+            "index.html",
+            inputGraph=graph,
+            inputKGCL=kgcl,
+            parsedKGCL=kgcl_model_rendering,
+            outputGraph=transformation,
+            examples=examples,
+        )
+
+    else:
+        # load rename example by default
+
+        # load graph
+        f = open("examples/kgcl/rename/graph.nt", "r")
+        graph = f.read()
+        f.close()
+
+        # load kgcl
+        f = open("examples/kgcl/rename/kgcl", "r")
+        kgcl = f.read()
+        f.close()
+
+        kgcl_transformation(graph, kgcl)
+
+        # parse KGCL input
+        kgcl_model = parse(kgcl)
+
+        # prepare parsed
+        kgcl_model_rendering = ""
+        for o in kgcl_model:
+            kgcl_model_rendering += render(o) + "\n"
+
+        # load transformed graph
+        f = open("examples/kgcl/tmp/transformation.nt", "r")
+        transformation = f.read()
+        f.close()
+
+        # return render_template("index.html", examples=examples)
+        return render_template(
+            "index.html",
+            inputGraph=graph,
+            inputKGCL=kgcl,
+            parsedKGCL=kgcl_model_rendering,
+            outputGraph=transformation,
+            examples=examples,
+        )
+
+
+@app.route("/diff", methods=["POST", "GET"])
+def diff():
+
+    examples = [
+        "rename",
+        "classCreation",
+        "obsoletion",
+        "unobsoletion",
+        "move",
+        "edgeCreation",
+        "edgeDeletion",
+    ]
+
+    # initialise variables
+    graph1 = ""
+    graph2 = ""
+    kgcl = ""
+
+    if request.method == "POST":
+
+        if "generate_diff" in request.form:
+            graph1 = request.form["graph1"]
+            graph2 = request.form["graph2"]
+
+            kgcl_diff(graph1, graph2)
+
+        if "load_example_diff" in request.form:
+            select = request.form.get("comp_select")
+            example = str(select)
+
+            f = open("examples/diff/" + example + "/graph1.nt", "r")
+            graph1 = f.read()
+            f.close()
+
+            f = open("examples/diff/" + example + "/graph2.nt", "r")
+            graph2 = f.read()
+            f.close()
+
+            kgcl_diff(graph1, graph2)
+
+        f = open("examples/kgcl/tmp/patch.kgcl", "r")
+        kgcl = f.read()
+        f.close()
+
+        return render_template(
+            "diff.html",
+            examples=examples,
+            kgclDiff=kgcl,
+            inputGraph1=graph1,
+            inputGraph2=graph2,
+        )
+    else:
+        # load rename by default
+        example = "rename"
+
+        f = open("examples/diff/" + example + "/graph1.nt", "r")
+        graph1 = f.read()
+        f.close()
+
+        f = open("examples/diff/" + example + "/graph2.nt", "r")
+        graph2 = f.read()
+        f.close()
+
+        kgcl_diff(graph1, graph2)
+
+        f = open("examples/kgcl/tmp/patch.kgcl", "r")
+        kgcl = f.read()
+        f.close()
+
+        return render_template(
+            "diff.html",
+            examples=examples,
+            kgclDiff=kgcl,
+            inputGraph1=graph1,
+            inputGraph2=graph2,
+        )
+
+
+# def parse(input):
+#     return parsing.parse(input)
+
+
+def kgcl_transformation(graph, kgcl):
+    # store graph as file
+    f = open("examples/kgcl/tmp/graph.nt", "w")
+    f.write(graph)
+    f.close()
+
+    # store kgcl statements
+    f = open("examples/kgcl/tmp/kgcl", "w")
+    f.write(kgcl)
+    f.close()
+
+    # parse KGCL input
+    parsed_statements = parse(kgcl)
+
+    # load input graph from file
+    g = rdflib.Graph()
+    g.load("examples/kgcl/tmp/graph.nt", format="nt")
+
+    # transform graph
+    apply_patch(parsed_statements, g)
+
+    # save graph
+    # TODO: get text representation of graph without writing to a file
+    g.serialize(destination="examples/kgcl/tmp/transformation.nt", format="nt")
+
+
+def kgcl_diff(graph1, graph2):
+    # store graph as file
+    f = open("examples/kgcl/tmp/graph1.nt", "w")
+    f.write(graph1)
+    f.close()
+
+    f = open("examples/kgcl/tmp/graph2.nt", "w")
+    f.write(graph2)
+    f.close()
+
+    # generate diff
+    # load graphs
+    g1 = rdflib.Graph()
+    g2 = rdflib.Graph()
+    g1.load("examples/kgcl/tmp/graph1.nt", format="nt")
+    g2.load("examples/kgcl/tmp/graph2.nt", format="nt")
+
+    # compute diff
+    existential_summary = existential.generate_atomic_existential_commands(g1, g2)
+    triple_annotation_summary = annotation.generate_triple_annotation_commands(g1, g2)
+    single_triple_summary = single.generate_thin_triple_commands(g1, g2)
+
+    # get KGCL commands
+    kgcl_commands = existential_summary.get_commands()
+    kgcl_commands += triple_annotation_summary.get_commands()
+    kgcl_commands += single_triple_summary.get_commands()
+
+    # pretty printing of KGCL commands
+    kgcl_commands = render_instances(kgcl_commands, g1)
+
+    # write KGCL commands
+    with open("examples/kgcl/tmp/patch.kgcl", "w") as f:
+        for k in kgcl_commands:
+            f.write(k)
+            f.write("\n")
+
+
+if __name__ == "__main__":
+    app.run(debug=True)
diff --git a/parser/testData/example/edgeCreation/graph.nt b/examples/diff/classCreation/graph1.nt
similarity index 100%
rename from parser/testData/example/edgeCreation/graph.nt
rename to examples/diff/classCreation/graph1.nt
diff --git a/examples/diff/classCreation/graph2.nt b/examples/diff/classCreation/graph2.nt
new file mode 100644
index 00000000..23f3b29e
--- /dev/null
+++ b/examples/diff/classCreation/graph2.nt
@@ -0,0 +1,1 @@
+<http://purl.obolibrary.org/obo/UBERON_0017671> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Class> .
diff --git a/parser/testData/example/nodeCreation/graph.nt b/examples/diff/edgeCreation/graph1.nt
similarity index 100%
rename from parser/testData/example/nodeCreation/graph.nt
rename to examples/diff/edgeCreation/graph1.nt
diff --git a/examples/diff/edgeCreation/graph2.nt b/examples/diff/edgeCreation/graph2.nt
new file mode 100644
index 00000000..3daa2970
--- /dev/null
+++ b/examples/diff/edgeCreation/graph2.nt
@@ -0,0 +1,1 @@
+<http://purl.obolibrary.org/obo/UBERON_0005875> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://purl.obolibrary.org/obo/UBERON_4000142> .
diff --git a/examples/diff/edgeDeletion/graph1.nt b/examples/diff/edgeDeletion/graph1.nt
new file mode 100644
index 00000000..3daa2970
--- /dev/null
+++ b/examples/diff/edgeDeletion/graph1.nt
@@ -0,0 +1,1 @@
+<http://purl.obolibrary.org/obo/UBERON_0005875> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://purl.obolibrary.org/obo/UBERON_4000142> .
diff --git a/examples/diff/edgeDeletion/graph2.nt b/examples/diff/edgeDeletion/graph2.nt
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/diff/move/graph1.nt b/examples/diff/move/graph1.nt
new file mode 100644
index 00000000..58e3dc76
--- /dev/null
+++ b/examples/diff/move/graph1.nt
@@ -0,0 +1,1 @@
+<http://purl.obolibrary.org/obo/UBERON_0005875> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://purl.obolibrary.org/obo/UBERON_0005879> .
diff --git a/examples/diff/move/graph2.nt b/examples/diff/move/graph2.nt
new file mode 100644
index 00000000..3daa2970
--- /dev/null
+++ b/examples/diff/move/graph2.nt
@@ -0,0 +1,1 @@
+<http://purl.obolibrary.org/obo/UBERON_0005875> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://purl.obolibrary.org/obo/UBERON_4000142> .
diff --git a/examples/diff/obsoletion/graph1.nt b/examples/diff/obsoletion/graph1.nt
new file mode 100644
index 00000000..96b0be15
--- /dev/null
+++ b/examples/diff/obsoletion/graph1.nt
@@ -0,0 +1,3 @@
+<http://purl.obolibrary.org/obo/UBERON_4000142> <http://www.w3.org/2000/01/rdf-schema#label> "metaplastic ossification" .
+<http://purl.obolibrary.org/obo/UBERON_4000142> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Class> .
+<http://purl.obolibrary.org/obo/UBERON_4000142> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://purl.obolibrary.org/obo/UBERON_0005879> .
diff --git a/examples/diff/obsoletion/graph2.nt b/examples/diff/obsoletion/graph2.nt
new file mode 100644
index 00000000..96be729b
--- /dev/null
+++ b/examples/diff/obsoletion/graph2.nt
@@ -0,0 +1,3 @@
+<http://purl.obolibrary.org/obo/UBERON_4000142> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Class> .
+<http://purl.obolibrary.org/obo/UBERON_4000142> <http://www.w3.org/2000/01/rdf-schema#label> "obsolete metaplastic ossification" .
+<http://purl.obolibrary.org/obo/UBERON_4000142> <http://www.w3.org/2002/07/owl#deprecated> "true"^^<http://www.w3.org/2001/XMLSchema#boolean> .
diff --git a/examples/diff/rename/graph1.nt b/examples/diff/rename/graph1.nt
new file mode 100644
index 00000000..35ca3fc0
--- /dev/null
+++ b/examples/diff/rename/graph1.nt
@@ -0,0 +1,1 @@
+<http://purl.obolibrary.org/obo/UBERON_0007176> <http://www.w3.org/2000/01/rdf-schema#label> "superior angle of scapula" .
diff --git a/examples/diff/rename/graph2.nt b/examples/diff/rename/graph2.nt
new file mode 100644
index 00000000..2a14994f
--- /dev/null
+++ b/examples/diff/rename/graph2.nt
@@ -0,0 +1,1 @@
+<http://purl.obolibrary.org/obo/UBERON_0007176> <http://www.w3.org/2000/01/rdf-schema#label> "inferior angle of scapula" .
diff --git a/examples/diff/unobsoletion/graph1.nt b/examples/diff/unobsoletion/graph1.nt
new file mode 100644
index 00000000..96be729b
--- /dev/null
+++ b/examples/diff/unobsoletion/graph1.nt
@@ -0,0 +1,3 @@
+<http://purl.obolibrary.org/obo/UBERON_4000142> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Class> .
+<http://purl.obolibrary.org/obo/UBERON_4000142> <http://www.w3.org/2000/01/rdf-schema#label> "obsolete metaplastic ossification" .
+<http://purl.obolibrary.org/obo/UBERON_4000142> <http://www.w3.org/2002/07/owl#deprecated> "true"^^<http://www.w3.org/2001/XMLSchema#boolean> .
diff --git a/examples/diff/unobsoletion/graph2.nt b/examples/diff/unobsoletion/graph2.nt
new file mode 100644
index 00000000..45bd5ad3
--- /dev/null
+++ b/examples/diff/unobsoletion/graph2.nt
@@ -0,0 +1,2 @@
+<http://purl.obolibrary.org/obo/UBERON_4000142> <http://www.w3.org/2000/01/rdf-schema#label> "metaplastic ossification" .
+<http://purl.obolibrary.org/obo/UBERON_4000142> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Class> .
diff --git a/examples/kgcl/addSubsumptionAxiom/graph.nt b/examples/kgcl/addSubsumptionAxiom/graph.nt
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/kgcl/addSubsumptionAxiom/kgcl b/examples/kgcl/addSubsumptionAxiom/kgcl
new file mode 100644
index 00000000..af4e88db
--- /dev/null
+++ b/examples/kgcl/addSubsumptionAxiom/kgcl
@@ -0,0 +1,1 @@
+create edge ex:subclass rdfs:subClassOf ex:superclass
diff --git a/examples/kgcl/changeRelationship/graph.nt b/examples/kgcl/changeRelationship/graph.nt
new file mode 100644
index 00000000..3725491f
--- /dev/null
+++ b/examples/kgcl/changeRelationship/graph.nt
@@ -0,0 +1,1 @@
+<http://example.org/subclass> <http://example.org/partOf> <http://example.org/superclass> .
diff --git a/examples/kgcl/changeRelationship/kgcl b/examples/kgcl/changeRelationship/kgcl
new file mode 100644
index 00000000..61a3c051
--- /dev/null
+++ b/examples/kgcl/changeRelationship/kgcl
@@ -0,0 +1,1 @@
+change relationship between ex:subclass and ex:superclass from ex:partOf to ex:hasPart
diff --git a/parser/testData/example/rename/graph.nt b/examples/kgcl/createSynonym/graph.nt
similarity index 100%
rename from parser/testData/example/rename/graph.nt
rename to examples/kgcl/createSynonym/graph.nt
diff --git a/examples/kgcl/createSynonym/kgcl b/examples/kgcl/createSynonym/kgcl
new file mode 100644
index 00000000..370d748d
--- /dev/null
+++ b/examples/kgcl/createSynonym/kgcl
@@ -0,0 +1,5 @@
+create synonym 'Virus' for obo:NCBITaxon_2
+create exact synonym 'Eubacteria' for obo:NCBITaxon_2
+create related synonym 'Disease' for obo:NCBITaxon_2
+create narrow synonym 'Bacterium' for obo:NCBITaxon_2
+create broad synonym 'Infection'@en for obo:NCBITaxon_2
diff --git a/parser/testData/example/move/graph.nt b/examples/kgcl/deleteSubsumptionAxiom/graph.nt
similarity index 100%
rename from parser/testData/example/move/graph.nt
rename to examples/kgcl/deleteSubsumptionAxiom/graph.nt
diff --git a/examples/kgcl/deleteSubsumptionAxiom/kgcl b/examples/kgcl/deleteSubsumptionAxiom/kgcl
new file mode 100644
index 00000000..cc36cbe1
--- /dev/null
+++ b/examples/kgcl/deleteSubsumptionAxiom/kgcl
@@ -0,0 +1,1 @@
+delete edge ex:targetClass rdfs:subClassOf ex:A
diff --git a/examples/kgcl/demo/example_graph.nt b/examples/kgcl/demo/example_graph.nt
new file mode 100644
index 00000000..85d87440
--- /dev/null
+++ b/examples/kgcl/demo/example_graph.nt
@@ -0,0 +1,4 @@
+<http://purl.obolibrary.org/obo/OBI_0002514> <http://www.w3.org/2000/01/rdf-schema#label> "peritoneal fluid specimen" .
+<http://purl.obolibrary.org/obo/OBI_0002514> <http://purl.obolibrary.org/obo/IAO_0000115> "A specimen that is derived from peritoneal fluid." .
+<http://purl.obolibrary.org/obo/OBI_0002514> <http://purl.obolibrary.org/obo/IAO_0000114> <http://purl.obolibrary.org/obo/IAO_0000122> .
+<http://purl.obolibrary.org/obo/OBI_0002514> <http://purl.obolibrary.org/obo/IAO_0000111> "peritoneal fluid specimen" .
diff --git a/examples/kgcl/demo/example_graph_version_2.nt b/examples/kgcl/demo/example_graph_version_2.nt
new file mode 100644
index 00000000..c1df29ca
--- /dev/null
+++ b/examples/kgcl/demo/example_graph_version_2.nt
@@ -0,0 +1,4 @@
+<http://purl.obolibrary.org/obo/OBI_0002514> <http://www.w3.org/2000/01/rdf-schema#label> "fluid specimen" .
+<http://purl.obolibrary.org/obo/OBI_0002514> <http://purl.obolibrary.org/obo/IAO_0000115> "A specimen that is derived from peritoneal fluid." .
+<http://purl.obolibrary.org/obo/OBI_0002514> <http://purl.obolibrary.org/obo/IAO_0000114> <http://purl.obolibrary.org/obo/IAO_0000122> .
+<http://purl.obolibrary.org/obo/OBI_0002514> <http://purl.obolibrary.org/obo/IAO_0000111> "peritoneal fluid specimen" .
diff --git a/examples/kgcl/demo/example_patch.kgcl b/examples/kgcl/demo/example_patch.kgcl
new file mode 100644
index 00000000..3c2e0fb6
--- /dev/null
+++ b/examples/kgcl/demo/example_patch.kgcl
@@ -0,0 +1,1 @@
+rename 'peritoneal fluid specimen' to 'fluid specimen'
diff --git a/examples/kgcl/demo/example_patch_2.kgcl b/examples/kgcl/demo/example_patch_2.kgcl
new file mode 100644
index 00000000..73747060
--- /dev/null
+++ b/examples/kgcl/demo/example_patch_2.kgcl
@@ -0,0 +1,16 @@
+rename 'aaa' to 'bbb'
+obsolete obo:Limb
+unobsolete obo:Limb
+delete obo:Limb
+move ex:S ex:P ex:O from ex:O to ex:B
+deepen ex:target from ex:old to ex:new
+shallow ex:target from ex:old to ex:new
+create edge ex:S ex:P ex:O
+delete edge ex:S ex:P ex:O
+change relationship between ex:S and ex:O from ex:X to ex:Y
+change annotation of ex:S with ex:P from ex:X to ex:Y
+create node ex:S 'new node'
+create ex:S
+create exact synonym 'new synonym' for ex:S
+create edge ex:S rdfs:subClassOf ex:O
+delete edge ex:S rdfs:subClassOf ex:O
diff --git a/examples/kgcl/edgeCreation/graph.nt b/examples/kgcl/edgeCreation/graph.nt
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/kgcl/edgeCreation/kgcl b/examples/kgcl/edgeCreation/kgcl
new file mode 100644
index 00000000..b5e63de2
--- /dev/null
+++ b/examples/kgcl/edgeCreation/kgcl
@@ -0,0 +1,1 @@
+create edge ex:subject ex:predicate ex:object
diff --git a/examples/kgcl/edgeDeletion/graph.nt b/examples/kgcl/edgeDeletion/graph.nt
new file mode 100644
index 00000000..0da06f9d
--- /dev/null
+++ b/examples/kgcl/edgeDeletion/graph.nt
@@ -0,0 +1,4 @@
+_:Ndb451c3aad3841c88d7042f29b20bea8 <http://www.w3.org/2002/07/owl#onProperty> <http://example.org/predicate> .
+_:Ndb451c3aad3841c88d7042f29b20bea8 <http://www.w3.org/2002/07/owl#someValuesFrom> <http://example.org/object> .
+_:Ndb451c3aad3841c88d7042f29b20bea8 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Restriction> .
+<http://example.org/subject> <http://www.w3.org/2000/01/rdf-schema#subClassOf> _:Ndb451c3aad3841c88d7042f29b20bea8 .
diff --git a/examples/kgcl/edgeDeletion/kgcl b/examples/kgcl/edgeDeletion/kgcl
new file mode 100644
index 00000000..ce52f73c
--- /dev/null
+++ b/examples/kgcl/edgeDeletion/kgcl
@@ -0,0 +1,1 @@
+delete edge ex:subject ex:predicate ex:object
diff --git a/examples/kgcl/move/graph.nt b/examples/kgcl/move/graph.nt
new file mode 100644
index 00000000..bc7794f6
--- /dev/null
+++ b/examples/kgcl/move/graph.nt
@@ -0,0 +1,1 @@
+<http://example.org/targetClass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/A> .
diff --git a/examples/kgcl/move/kgcl b/examples/kgcl/move/kgcl
new file mode 100644
index 00000000..d1e0bc4f
--- /dev/null
+++ b/examples/kgcl/move/kgcl
@@ -0,0 +1,1 @@
+move ex:targetClass rdfs:subClassOf ex:A from ex:A to ex:B
diff --git a/examples/kgcl/nodeCreation/graph.nt b/examples/kgcl/nodeCreation/graph.nt
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/kgcl/nodeCreation/kgcl b/examples/kgcl/nodeCreation/kgcl
new file mode 100644
index 00000000..5c9882c6
--- /dev/null
+++ b/examples/kgcl/nodeCreation/kgcl
@@ -0,0 +1,2 @@
+create node obo:NCBITaxon_2 'Bacteria'
+create obo:NCBITaxon_4
diff --git a/parser/testData/example/nodeDeepening/graph.nt b/examples/kgcl/nodeDeepening/graph.nt
similarity index 100%
rename from parser/testData/example/nodeDeepening/graph.nt
rename to examples/kgcl/nodeDeepening/graph.nt
diff --git a/examples/kgcl/nodeDeepening/kgcl b/examples/kgcl/nodeDeepening/kgcl
new file mode 100644
index 00000000..42a1c1ff
--- /dev/null
+++ b/examples/kgcl/nodeDeepening/kgcl
@@ -0,0 +1,1 @@
+deepen ex:targetClass from ex:superclass to ex:subclass
diff --git a/parser/testData/example/nodeDeletionById/.graph.nt.swp b/examples/kgcl/nodeDeletionById/.graph.nt.swp
similarity index 100%
rename from parser/testData/example/nodeDeletionById/.graph.nt.swp
rename to examples/kgcl/nodeDeletionById/.graph.nt.swp
diff --git a/parser/testData/example/nodeDeletionById/graph.nt b/examples/kgcl/nodeDeletionById/graph.nt
similarity index 100%
rename from parser/testData/example/nodeDeletionById/graph.nt
rename to examples/kgcl/nodeDeletionById/graph.nt
diff --git a/parser/testData/example/nodeDeletionById/kgcl b/examples/kgcl/nodeDeletionById/kgcl
similarity index 100%
rename from parser/testData/example/nodeDeletionById/kgcl
rename to examples/kgcl/nodeDeletionById/kgcl
diff --git a/parser/testData/example/nodeDeletionByLabel/graph.nt b/examples/kgcl/nodeDeletionByLabel/graph.nt
similarity index 100%
rename from parser/testData/example/nodeDeletionByLabel/graph.nt
rename to examples/kgcl/nodeDeletionByLabel/graph.nt
diff --git a/parser/testData/example/nodeDeletionByLabel/kgcl b/examples/kgcl/nodeDeletionByLabel/kgcl
similarity index 100%
rename from parser/testData/example/nodeDeletionByLabel/kgcl
rename to examples/kgcl/nodeDeletionByLabel/kgcl
diff --git a/parser/testData/example/nodeShallowing/graph.nt b/examples/kgcl/nodeShallowing/graph.nt
similarity index 100%
rename from parser/testData/example/nodeShallowing/graph.nt
rename to examples/kgcl/nodeShallowing/graph.nt
diff --git a/examples/kgcl/nodeShallowing/kgcl b/examples/kgcl/nodeShallowing/kgcl
new file mode 100644
index 00000000..abb6ac95
--- /dev/null
+++ b/examples/kgcl/nodeShallowing/kgcl
@@ -0,0 +1,1 @@
+shallow ex:targetClass from ex:subclass to ex:superclass
diff --git a/parser/testData/example/obsoleteById/graph.nt b/examples/kgcl/obsoleteById/graph.nt
similarity index 100%
rename from parser/testData/example/obsoleteById/graph.nt
rename to examples/kgcl/obsoleteById/graph.nt
diff --git a/examples/kgcl/obsoleteById/kgcl b/examples/kgcl/obsoleteById/kgcl
new file mode 100644
index 00000000..081760cd
--- /dev/null
+++ b/examples/kgcl/obsoleteById/kgcl
@@ -0,0 +1,1 @@
+obsolete obo:NCBITaxon_2
diff --git a/parser/testData/example/obsoleteByLabel/graph.nt b/examples/kgcl/obsoleteByLabel/graph.nt
similarity index 100%
rename from parser/testData/example/obsoleteByLabel/graph.nt
rename to examples/kgcl/obsoleteByLabel/graph.nt
diff --git a/parser/testData/example/obsoleteByLabel/kgcl b/examples/kgcl/obsoleteByLabel/kgcl
similarity index 100%
rename from parser/testData/example/obsoleteByLabel/kgcl
rename to examples/kgcl/obsoleteByLabel/kgcl
diff --git a/parser/testData/graph.nt b/examples/kgcl/rename/graph.nt
similarity index 100%
rename from parser/testData/graph.nt
rename to examples/kgcl/rename/graph.nt
diff --git a/parser/testData/example/rename/kgcl b/examples/kgcl/rename/kgcl
similarity index 100%
rename from parser/testData/example/rename/kgcl
rename to examples/kgcl/rename/kgcl
diff --git a/examples/kgcl/tmp/graph.nt b/examples/kgcl/tmp/graph.nt
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/kgcl/tmp/graph1.nt b/examples/kgcl/tmp/graph1.nt
new file mode 100644
index 00000000..3daa2970
--- /dev/null
+++ b/examples/kgcl/tmp/graph1.nt
@@ -0,0 +1,1 @@
+<http://purl.obolibrary.org/obo/UBERON_0005875> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://purl.obolibrary.org/obo/UBERON_4000142> .
diff --git a/examples/kgcl/tmp/graph2.nt b/examples/kgcl/tmp/graph2.nt
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/kgcl/tmp/kgcl b/examples/kgcl/tmp/kgcl
new file mode 100644
index 00000000..42e38640
--- /dev/null
+++ b/examples/kgcl/tmp/kgcl
@@ -0,0 +1,1 @@
+add <http://example.org/subclass> SubClassOf <http://example.org/superclass>
diff --git a/examples/kgcl/tmp/patch.kgcl b/examples/kgcl/tmp/patch.kgcl
new file mode 100644
index 00000000..120702d4
--- /dev/null
+++ b/examples/kgcl/tmp/patch.kgcl
@@ -0,0 +1,1 @@
+delete <http://purl.obolibrary.org/obo/UBERON_0005875> SubClassOf <http://purl.obolibrary.org/obo/UBERON_4000142>
diff --git a/examples/kgcl/tmp/transformation.nt b/examples/kgcl/tmp/transformation.nt
new file mode 100644
index 00000000..764906aa
--- /dev/null
+++ b/examples/kgcl/tmp/transformation.nt
@@ -0,0 +1,2 @@
+<http://example.org/subclass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/superclass> .
+
diff --git a/parser/testData/example/unobsoleteById/graph.nt b/examples/kgcl/unobsoleteById/graph.nt
similarity index 100%
rename from parser/testData/example/unobsoleteById/graph.nt
rename to examples/kgcl/unobsoleteById/graph.nt
diff --git a/examples/kgcl/unobsoleteById/kgcl b/examples/kgcl/unobsoleteById/kgcl
new file mode 100644
index 00000000..b2ef0c04
--- /dev/null
+++ b/examples/kgcl/unobsoleteById/kgcl
@@ -0,0 +1,1 @@
+unobsolete obo:NCBITaxon_2
diff --git a/kgcl/README.md b/kgcl/README.md
new file mode 100644
index 00000000..5a41ee62
--- /dev/null
+++ b/kgcl/README.md
@@ -0,0 +1,186 @@
+# KGCL Tooling
+This folder contains tooling for handling change operations in ontologies according to the [Knowledge-Graph-Change-Language](https://cmungall.github.io/knowledge-graph-change-language/).
+
+**The code is in active development and subject to frequent changes.**
+
+**Feedback and PRs are welcome! There is always more to do and things to improve.**
+
+## tl;dr
+
+Example code for applying a KGCL patch to a graph.
+The examples make use of relative paths.
+So, they need to be located in the root folder of this project (i.e., knowledge-graph-change-language - the parent folder of this folder).
+
+```
+import rdflib
+from rdflib.util import guess_format
+import kgcl.grammar.parser
+import kgcl.apply.graph_transformer
+
+#graph: path to an RDF graph
+#patch_file: file containing a set of KGCL commands
+#output: path to an output destination
+
+# read kgcl commands from file
+patch_file = open("examples/kgcl/demo/example_patch.kgcl", "r")
+patch = patch_file.read()
+
+# parse kgcl commands
+parsed_patch = kgcl.grammar.parser.parse(patch)
+
+#load RDF graph
+path_to_graph = "examples/kgcl/demo/example_graph.nt"
+g = rdflib.Graph()
+g.load(path_to_graph, format=guess_format(path_to_graph))
+
+# apply kgcl commands as SPARQL UPDATE queries to graph
+kgcl.apply.graph_transformer.apply_patch(parsed_patch, g)
+
+# save updated graph
+output = "output.nt"
+g.serialize(destination=output, format="nt") 
+```
+
+Example code for generating a KGCL Diff between two graphs:
+
+```
+from kgcl.diff.summary_generation import run
+
+#graph_1: path to an RDF graph
+#graph_2: path to an RDF graph
+#output: path to an output destination (which must not exist as a folder)
+
+graph_1 = "examples/kgcl/demo/example_graph.nt"
+graph_2 = "examples/kgcl/demo/example_graph_version_2.nt"
+output = "output"
+
+run(graph_1, graph_2, output)
+```
+
+Example code for generating a KGCL Diff between two graphs (without writing a summary to an output folder): 
+
+```
+import rdflib
+
+import kgcl.diff.diff_2_kgcl_single as single
+import kgcl.diff.diff_2_kgcl_existential as existential
+
+#graph_1: path to an RDF graph
+#graph_2: path to an RDF graph
+#output: path to an output destination (which must not exist as a folder)
+
+graph_1 = "examples/kgcl/demo/example_graph.nt"
+graph_2 = "examples/kgcl/demo/example_graph_version_2.nt"
+
+#load RDF graphs
+g_1 = rdflib.Graph()
+g_1.load(graph_1, format=guess_format(graph_1))
+g_2 = rdflib.Graph()
+g_2.load(graph_2, format=guess_format(graph_2))
+
+#generate diff
+single_triple_summary = single.generate_thin_triple_commands(g1, g2)
+existential_summary = existential.generate_atomic_existential_commands(g1, g2) 
+
+# get KGCL commands
+kgcl_commands = existential_summary.get_commands()
+kgcl_commands += single_triple_summary.get_commands()
+``` 
+
+Example code for converting a KGCL patch into RDF:
+```
+from kgcl.grammar.kgcl_2_rdf import kgcl_2_yaml
+from kgcl.utils import from_yaml, to_rdf
+from rdflib import Graph
+
+patch_file = open("examples/kgcl/demo/example_patch_2.kgcl", "r")
+patch = patch_file.read() 
+output = "output"
+
+#this parses a KGCL into KGCL instances of the KGCL data model
+#encodes them in YAML,
+#and writes them to output
+kgcl_2_yaml(patch, output)
+
+#this loads the YAML encoding
+#and translates it into RDF
+session = from_yaml("output")
+G: Graph
+G = to_rdf(session)
+G.serialize("kgcl_patch.rdf", format="turtle") 
+```
+
+## What are KGCL change operations?
+
+A KGCL change operation is a statement that specifies a change for a knowledge graph (or ontology). 
+For example, the statment `add A SubClassOf B` specifies the addition of a subsumption axiom between two (named) classes `A` and `B`.
+The current prototype supports the following KGCL change operations (according to this [grammar](https://github.com/ckindermann/knowledge-graph-change-language/blob/parser/kgcl_tool/grammar/kgcl.lark)):
+- renaming labels
+- creating nodes
+- deleting nodes
+- obsoleting entities
+- unobsoleting entities
+- deepening nodes
+- shallowing nodes
+- creating synonyms
+- moving nodes
+- changing relationships 
+- creating (annotated) edges
+- deleting (annotated) edges
+- adding subsumption axioms
+- deleting subsumption axioms
+- adding atomic existential restriction axioms
+- deleting atomic existential restriction axioms 
+
+## How do I use KGCL operations?
+
+A (text) file containing KGCL operations in each line is called a _patch_. A patch can be applied to an `input.nt` knowledge graph (serialised as N-triples) by calling `python kgcl.py input.nt patch output.nt` to generate the `output.n` knowledge graph.
+ 
+## How can I specify test cases for KGCL?
+
+A _test_ for KGCL can be specified by creating a folder containing three files:
+1. `inputGraph.nt` (the input knowledge graph)
+2. `kgcl` (the KGCL patch)
+3. `outputGraph.nt` (the expected output knowledge graph).  
+
+Calling `python custom_kgcl_tests.py` executes all tests in the folder [`tests/data/kgcl`](https://github.com/ckindermann/knowledge-graph-change-language/tree/parser/kgcl_tool/tests/data/kgcl).
+
+## What is a KGCL Diff?
+
+A KGCL Diff of two knowledge graphs `g1.nt` and `g2.nt` is a KGCL patch `p` such that `p` applied to `g1.nt` yields `g2.nt`.
+
+## How do I generate a KGCL Diff?
+
+**Note: the current implementation does not generate a KGCL Diff as defined above. KGCL Diff does not support all change operations that are already available in KGCL. In particular, most OWL axioms involving blank nodes are currently ignored.**
+
+A KGLC Diff can be generated by calling `python kgcl_diff.py g1.nt g2.nt output` where `output` is a path to the desired output directory (note that this directory will be created and must not already exist). Besides the KGCL patch, the `output` folder will contain supplementary information about the generated patch.
+
+## What is an incomplete KGCL Diff useful for?
+
+Even an incomplete KGCL Diff, i.e. a KGCL patch that doesn't account for all changes between two knowledge graphes, can provide useful information about how to versions of a knowledge graph differ.
+For example, consider an update for a knowledge graph in which only a language tag, say `en`, to labels have been added. In this case, a KGCL Diff is generated that only consists of KGCL change operations
+of the form `rename node from x to x@en`. The current implementation provides a summary report about the number of different change operations involved in a KGCL Diff (keeping in mind that KGCL Diff does not yet support all operations from KGCL).
+
+# Installation
+
+This project uses [pipenv](https://pipenv-fork.readthedocs.io/en/latest/) for installation.
+
+ `pipenv install kgcl`
+
+# How to contribute?
+
+There are many things left to do. 
+- testing both KGCL and KGCL Diff (beyond simple unit testing)
+- extending the grammar (there are many missing KGCL operations)
+- making KGCL commands more user-friendly and forgiving (i.e. by allowing the use of labels instead of IRIs)
+- building a GUI (start with a Jupyter Notebook?)
+- building a (proper) CLI (that integrates KGCL and KGCL Diff with proper subgroups)
+- extending KGCL data model to handle general OWL axioms (or at least OWL EL)
+- package the project for easy installation
+- handling of non-deterministic diffs
+- robust handling of complex KGCL change operations (consider the case of obsoletions - when can a class be considerd 'obsoleted'? If it's label chaged "obsolete X"? If there is a triple with `oboInOwl:deprecated` set to `true`? Are both conditions sufficient by themselves?)
+- (proper) parsing of URIs and CURIES 
+- validating constraints on KGCL operations
+- setting up Github Actions
+- change ontologies via GitHub tickets (using KGCL commands)
+- cleaning up the code base (many things are not yet pythonic - if something seems off to you, it's because it probably is)
diff --git a/kgcl/__init__.py b/kgcl/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/kgcl/apply/__init__.py b/kgcl/apply/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/kgcl/apply/__pycache__/__init__.cpython-38.pyc b/kgcl/apply/__pycache__/__init__.cpython-38.pyc
new file mode 100644
index 00000000..2fa81cdc
Binary files /dev/null and b/kgcl/apply/__pycache__/__init__.cpython-38.pyc differ
diff --git a/kgcl/apply/__pycache__/graph_transformer.cpython-38.pyc b/kgcl/apply/__pycache__/graph_transformer.cpython-38.pyc
new file mode 100644
index 00000000..3c5d7d0d
Binary files /dev/null and b/kgcl/apply/__pycache__/graph_transformer.cpython-38.pyc differ
diff --git a/kgcl/apply/__pycache__/kgcl_2_sparql.cpython-38.pyc b/kgcl/apply/__pycache__/kgcl_2_sparql.cpython-38.pyc
new file mode 100644
index 00000000..dc187825
Binary files /dev/null and b/kgcl/apply/__pycache__/kgcl_2_sparql.cpython-38.pyc differ
diff --git a/kgcl/apply/graph_transformer.py b/kgcl/apply/graph_transformer.py
new file mode 100644
index 00000000..d61af2a6
--- /dev/null
+++ b/kgcl/apply/graph_transformer.py
@@ -0,0 +1,12 @@
+from .kgcl_2_sparql import convert
+
+
+def apply_patch(kgcl_instances, graph):
+    for i in kgcl_instances:
+        query = convert(i)
+        graph.update(query)
+
+
+def apply_command(kgcl_instance, graph):
+    query = convert(kgcl_instance)
+    graph.update(query)
diff --git a/kgcl/apply/kgcl_2_sparql.py b/kgcl/apply/kgcl_2_sparql.py
new file mode 100644
index 00000000..b3d0fc74
--- /dev/null
+++ b/kgcl/apply/kgcl_2_sparql.py
@@ -0,0 +1,1759 @@
+import re
+from kgcl.model.kgcl import (
+    NodeRename,
+    NodeObsoletion,
+    NodeUnobsoletion,
+    NodeDeletion,
+    NodeMove,
+    NodeDeepening,
+    NodeShallowing,
+    NodeAnnotationChange,
+    EdgeCreation,
+    EdgeDeletion,
+    PredicateChange,
+    NodeCreation,
+    ClassCreation,
+    NewSynonym,
+    RemovedNodeFromSubset,
+    PlaceUnder,
+    RemoveUnder,
+)
+
+
+def get_prefix(curie):
+    return curie.split(":")[0]
+
+
+# TODO; JSON LD obo context look at robot
+prefix_2_uri = {
+    "obo": "<http://purl.obolibrary.org/obo/>",
+    "ex": "<http://example.org/>",
+    "oboInOwl": "<http://www.geneontology.org/formats/oboInOwl#>",
+    "rdfs": "<http://www.w3.org/2000/01/rdf-schema#>",
+    "xsd": "<http://www.w3.org/2001/XMLSchema#>",
+    "rdf": "<http://www.w3.org/1999/02/22-rdf-syntax-ns#>",
+    "owl": "<http://www.w3.org/2002/07/owl#>",
+    "swrl": "<http://www.w3.org/2003/11/swrl#>",
+    "oio": "<http://www.geneontology.org/formats/oboInOwl#>",
+    "dce": "<http://purl.org/dc/elements/1.1/>",
+    "dct": "<http://purl.org/dc/terms/>",
+    "foaf": "<http://xmlns.com/foaf/0.1/>",
+    "protege": "<http://protege.stanford.edu/plugins/owl/protege#>",
+    "BFO": "<http://purl.obolibrary.org/obo/BFO_>",
+    "CHEBI": "<http://purl.obolibrary.org/obo/CHEBI_>",
+    "CL": "<http://purl.obolibrary.org/obo/CL_>",
+    "IAO": "<http://purl.obolibrary.org/obo/IAO_>",
+    "NCBITaxon": "<http://purl.obolibrary.org/obo/NCBITaxon_>",
+    "OBI": "<http://purl.obolibrary.org/obo/OBI_>",
+    "PR": "<http://purl.obolibrary.org/obo/PR_>",
+    "obo": "<http://purl.obolibrary.org/obo/>",
+    "UP": "<http://purl.uniprot.org/uniprot/>",
+    "UC": "<http://purl.uniprot.org/core/>",
+    "PRO": "<http://www.uniprot.org/annotation/PRO_>",
+    "faldo": "<http://biohackathon.org/resource/faldo#>",
+    # TODO add more prefixes
+}
+
+
+def is_label(input):
+    return re.match(r"\'[^ \s\'].*\'", input)
+
+
+def is_id(input):
+    return re.match(r"<\S+>", input)
+
+
+def build_curie_prefix(entity):
+    curie_prefix = get_prefix(entity)
+    curie_uri = prefix_2_uri[curie_prefix]
+    return "PREFIX " + curie_prefix + ": " + curie_uri + " "
+
+
+# TODO proper escape handling
+def escape_literal(literal):
+    return literal.replace("\\", "\\\\").replace('"', '\\"')
+    # .replace("\\'", "\\\\'")
+
+
+def convert(kgcl_instance):
+    """Given a KGCL dataclass,
+    return a SPARQL UPDATE query corresponding to the encoded change.
+    """
+
+    # label renaming
+    if type(kgcl_instance) is NodeRename:
+        return rename(kgcl_instance)
+
+    # node obsoletion
+    if type(kgcl_instance) is NodeObsoletion:
+        representation = kgcl_instance.about_node_representation
+        if representation == "uri":
+            return obsolete_by_id(kgcl_instance)
+        if representation == "label":
+            return obsolete_by_label(kgcl_instance)
+        if representation == "curie":
+            return obsolete_curie(kgcl_instance)
+
+    # node obsoletion
+    if type(kgcl_instance) is NodeUnobsoletion:
+        representation = kgcl_instance.about_node_representation
+        if representation == "uri":
+            return unobsolete_by_id(kgcl_instance)
+        if representation == "label":
+            return unobsolete_by_label(kgcl_instance)
+        if representation == "curie":
+            return unobsolete_curie(kgcl_instance)
+
+    # node deletion
+    if type(kgcl_instance) is NodeDeletion:
+        representation = kgcl_instance.about_node_representation
+        if representation == "uri":
+            return delete_by_id(kgcl_instance)
+        if representation == "label":
+            return delete_by_label(kgcl_instance)
+        if representation == "curie":
+            return delete_curie(kgcl_instance)
+
+    # node creation
+    if type(kgcl_instance) is NodeCreation:
+        return create_node(kgcl_instance)
+
+    # class creation
+    if type(kgcl_instance) is ClassCreation:
+        return create_class(kgcl_instance)
+
+    # node deepending
+    if type(kgcl_instance) is NodeDeepening:
+        return node_deepening(kgcl_instance)
+
+    # node shallowing
+    if type(kgcl_instance) is NodeShallowing:
+        return node_shallowing(kgcl_instance)
+
+    if type(kgcl_instance) is NodeAnnotationChange:
+        return node_annotation_change(kgcl_instance)
+
+    # node move
+    if type(kgcl_instance) is NodeMove:
+        return node_move(kgcl_instance)
+
+    if type(kgcl_instance) is NewSynonym:
+        representation = kgcl_instance.about_node_representation
+
+        if representation == "uri":
+            return new_synonym_for_uri(kgcl_instance)
+        if representation == "label":
+            return new_synonym_for_label(kgcl_instance)
+        if representation == "curie":
+            return new_synonym_for_curie(kgcl_instance)
+
+    if type(kgcl_instance) is PredicateChange:
+        return change_predicate(kgcl_instance)
+
+    if type(kgcl_instance) is RemovedNodeFromSubset:
+        if is_id(kgcl_instance.about_node) and is_id(kgcl_instance.subset):
+            return remove_node_from_subset(kgcl_instance)
+
+    if type(kgcl_instance) is PlaceUnder:
+        return triple_creation(kgcl_instance)
+
+    if type(kgcl_instance) is RemoveUnder:
+        return triple_deletion(kgcl_instance)
+
+    if type(kgcl_instance) is EdgeCreation:
+        return create_existential_restriction(kgcl_instance)
+
+    if type(kgcl_instance) is EdgeDeletion:
+        return delete_existential_restriction(kgcl_instance)
+
+
+def node_move(kgcl_instance):
+
+    # NB: object and old_value are the (necessarily) the same
+    subject = kgcl_instance.about_edge.subject
+    predicate = kgcl_instance.about_edge.predicate
+    object = kgcl_instance.about_edge.object
+
+    subject_type = kgcl_instance.about_edge.subject_representation
+    predicate_type = kgcl_instance.about_edge.predicate_representation
+    object_type = kgcl_instance.about_edge.object_representation
+
+    old_value = kgcl_instance.old_value
+    new_value = kgcl_instance.new_value
+
+    old_type = kgcl_instance.old_object_type
+    new_type = kgcl_instance.new_object_type
+
+    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>  "
+    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
+    prefix += "PREFIX xsd:  <http://www.w3.org/2001/XMLSchema#> "
+
+    # set up prefixes  for curies as needed
+    if subject_type == "curie":
+        prefix += build_curie_prefix(subject)
+
+    if predicate_type == "curie":
+        prefix += build_curie_prefix(predicate)
+
+    if object_type == "curie":
+        prefix += build_curie_prefix(object)
+
+    if old_type == "curie":
+        prefix += build_curie_prefix(old_value)
+
+    if new_type == "curie":
+        prefix += build_curie_prefix(new_value)
+
+    deleteQuery = "?subject ?predicate ?old . "
+
+    delete = "DELETE {" + deleteQuery + "}"
+
+    insertQuery = "?subject ?predicate ?new . "
+
+    insert = "INSERT {" + insertQuery + "}"
+
+    whereQuery = ""
+
+    if old_type == "label":
+        whereQuery += "?old rdfs:label ?old_label . "
+        whereQuery += ' FILTER(STR(?old_label)="' + old_value + '") '
+    else:
+        whereQuery += " BIND(" + old_value + " AS ?old) "
+
+    if new_type == "label":
+        whereQuery += "?new rdfs:label ?new_label . "
+        whereQuery += ' FILTER(STR(?new_label)="' + new_value + '") '
+    else:
+        whereQuery += " BIND(" + new_value + " AS ?new) "
+
+    if subject_type == "label":
+        whereQuery += "?subject rdfs:label ?entity_label . "
+        whereQuery += ' FILTER(STR(?entity_label)="' + subject + '") '
+    else:
+        whereQuery += " BIND(" + subject + " AS ?subject) "
+
+    if predicate_type == "label":
+        whereQuery += "?predicate rdfs:label ?predicate_label . "
+        whereQuery += ' FILTER(STR(?predicate_label)="' + predicate + '") '
+    else:
+        whereQuery += " BIND(" + predicate + " AS ?predicate) "
+
+    whereQuery += "?subject ?predicate ?old . "
+
+    where = "WHERE {" + whereQuery + "}"
+
+    updateQuery = prefix + " " + delete + " " + insert + " " + where
+
+    return updateQuery
+
+    updateQuery = prefix + " " + delete + " " + insert + " " + where
+
+    return updateQuery
+
+
+def remove_node_from_subset(kgcl_instance):
+
+    about = kgcl_instance.about_node
+    subset = kgcl_instance.subset
+
+    updateQuery = (
+        f"PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>"
+        f"PREFIX obo: <http://purl.obolibrary.org/obo/>"
+        f"DELETE {{ {about} obo:inSubset {subset} }}"
+        f"WHERE {{ {about} obo:inSubset {subset} }}"
+    )
+
+    # prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>  "
+    # prefix += "PREFIX obo: <http://purl.obolibrary.org/obo/> "
+
+    # deleteQuery = about + " obo:inSubset " + subset + " . "
+
+    # delete = "DELETE {" + deleteQuery + "}"
+
+    # whereQuery = about + " obo:inSubset " + subset + " . "
+
+    # where = "WHERE { " + whereQuery + " }"
+
+    # updateQuery = prefix + " " + delete + " " + where
+
+    return updateQuery
+
+
+def change_predicate(kgcl_instance):
+
+    subject = kgcl_instance.about_edge.subject
+    object = kgcl_instance.about_edge.object
+
+    old_value = kgcl_instance.old_value
+    new_value = kgcl_instance.new_value
+
+    language = kgcl_instance.language
+    datatype = kgcl_instance.datatype
+
+    subject_type = kgcl_instance.about_edge.subject_representation
+    object_type = kgcl_instance.about_edge.object_representation
+
+    old_value_type = kgcl_instance.old_value_type
+    new_value_type = kgcl_instance.new_value_type
+
+    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>  "
+
+    if subject_type == "curie":
+        prefix += build_curie_prefix(subject)
+
+    if object_type == "curie":
+        prefix += build_curie_prefix(object)
+
+    if old_value_type == "curie":
+        prefix += build_curie_prefix(old_value)
+
+    if new_value_type == "curie":
+        prefix += build_curie_prefix(new_value)
+
+    deleteQuery = "?subject ?old ?object . "
+
+    delete = "DELETE {" + deleteQuery + "}"
+
+    insertQuery = "?subject ?new ?object . "
+
+    insert = "INSERT {" + insertQuery + "}"
+
+    whereQuery = deleteQuery
+
+    if subject_type == "label":
+        whereQuery += "?subject rdfs:label ?subject_label . "
+        whereQuery += ' FILTER(STR(?subject_label)="' + subject + '") '
+
+    if subject_type == "uri" or subject_type == "curie":
+        whereQuery += " BIND(" + subject + " AS ?subject) "
+
+    if object_type == "label":
+        whereQuery += "?object rdfs:label ?object_label . "
+        whereQuery += ' FILTER(STR(?object_label)="' + object + '") '
+
+    if object_type == "uri" or object_type == "curie":
+        whereQuery += " BIND(" + object + " AS ?object) "
+
+    if object_type == "literal":
+        object = escape_literal(object)
+        if datatype is not None:
+            # TODO: accept CURIES for data types
+            whereQuery += ' BIND( STRDT("' + object + '",' + datatype + ") AS ?object) "
+        elif language is not None:
+            whereQuery += (
+                'BIND( STRLANG("' + object + '","' + language + '") AS ?object) '
+            )
+        else:
+            whereQuery += 'BIND("' + object + '" AS ?object)'
+
+    if new_value_type == "label":
+        whereQuery += "?new rdfs:label ?new_label . "
+        whereQuery += ' FILTER(STR(?new_label)="' + new_value + '") '
+    else:
+        whereQuery += " BIND(" + new_value + " AS ?new) "
+
+    if old_value_type == "label":
+        whereQuery += "?old rdfs:label ?old_label . "
+        whereQuery += ' FILTER(STR(?old_label)="' + old_value + '") '
+    else:
+        whereQuery += " BIND(" + old_value + " AS ?old) "
+
+    where = "WHERE {" + whereQuery + "}"
+
+    updateQuery = prefix + " " + delete + " " + insert + " " + where
+
+    return updateQuery
+
+
+def node_deepening(kgcl_instance):
+
+    entity = kgcl_instance.about_edge.subject
+    old_value = kgcl_instance.old_value
+    new_value = kgcl_instance.new_value
+
+    entity_type = kgcl_instance.about_edge.subject_representation
+    old_type = kgcl_instance.old_object_type
+    new_type = kgcl_instance.new_object_type
+
+    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>  "
+    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
+    prefix += "PREFIX xsd:  <http://www.w3.org/2001/XMLSchema#> "
+
+    # set up prefixes  for curies as needed
+    if entity_type == "curie":
+        prefix += build_curie_prefix(entity)
+
+    if old_type == "curie":
+        prefix += build_curie_prefix(old_value)
+
+    if new_type == "curie":
+        prefix += build_curie_prefix(new_value)
+
+    deleteQuery = "?entity ?relation ?old . "
+
+    delete = "DELETE {" + deleteQuery + "}"
+
+    insertQuery = "?entity ?relation ?new . "
+
+    insert = "INSERT {" + insertQuery + "}"
+
+    whereQuery = ""
+
+    if old_type == "label":
+        whereQuery += "?old rdfs:label ?old_label . "
+        whereQuery += ' FILTER(STR(?old_label)="' + old_value + '") '
+    else:
+        whereQuery += " BIND(" + old_value + " AS ?old) "
+
+    if new_type == "label":
+        whereQuery += "?new rdfs:label ?new_label . "
+        whereQuery += ' FILTER(STR(?new_label)="' + new_value + '") '
+    else:
+        whereQuery += " BIND(" + new_value + " AS ?new) "
+
+    if entity_type == "label":
+        whereQuery += "?entity rdfs:label ?entity_label . "
+        whereQuery += ' FILTER(STR(?entity_label)="' + entity + '") '
+    else:
+        whereQuery += " BIND(" + entity + " AS ?entity) "
+
+    whereQuery += "?entity ?relation ?old . "
+    whereQuery += "?new ?relation ?old . "
+
+    where = "WHERE {" + whereQuery + "}"
+
+    updateQuery = prefix + " " + delete + " " + insert + " " + where
+
+    return updateQuery
+
+
+def node_shallowing(kgcl_instance):
+
+    entity = kgcl_instance.about_edge.subject
+    old_value = kgcl_instance.old_value
+    new_value = kgcl_instance.new_value
+
+    entity_type = kgcl_instance.about_edge.subject_representation
+    old_type = kgcl_instance.old_object_type
+    new_type = kgcl_instance.new_object_type
+
+    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>  "
+    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
+    prefix += "PREFIX xsd:  <http://www.w3.org/2001/XMLSchema#> "
+
+    if entity_type == "curie":
+        prefix += build_curie_prefix(entity)
+
+    if old_type == "curie":
+        prefix += build_curie_prefix(old_value)
+
+    if new_type == "curie":
+        prefix += build_curie_prefix(new_value)
+
+    deleteQuery = "?entity ?relation ?old . "
+
+    delete = "DELETE {" + deleteQuery + "}"
+
+    insertQuery = "?entity ?relation ?new . "
+
+    insert = "INSERT {" + insertQuery + "}"
+
+    whereQuery = ""
+
+    if old_type == "label":
+        whereQuery += "?old rdfs:label ?old_label . "
+        whereQuery += ' FILTER(STR(?old_label)="' + old_value + '") '
+    else:
+        whereQuery += " BIND(" + old_value + " AS ?old) "
+
+    if new_type == "label":
+        whereQuery += "?new rdfs:label ?new_label . "
+        whereQuery += ' FILTER(STR(?new_label)="' + new_value + '") '
+    else:
+        whereQuery += " BIND(" + new_value + " AS ?new) "
+
+    if entity_type == "label":
+        whereQuery += "?entity rdfs:label ?entity_label . "
+        whereQuery += ' FILTER(STR(?entity_label)="' + entity + '") '
+    else:
+        whereQuery += " BIND(" + entity + " AS ?entity) "
+
+    whereQuery += "?entity ?relation ?old . "
+    whereQuery += "?old ?relation ?new . "
+
+    where = "WHERE {" + whereQuery + "}"
+
+    updateQuery = prefix + " " + delete + " " + insert + " " + where
+
+    return updateQuery
+
+    updateQuery = prefix + " " + delete + " " + insert + " " + where
+
+    return updateQuery
+
+
+# TODO: handling of language tags
+# look things up at https://www.ebi.ac.uk/ols/ontologies/iao
+def unobsolete_by_id(kgcl_instance):
+    about = kgcl_instance.about_node
+    # http://wiki.geneontology.org/index.php/Restoring_an_Obsolete_Ontology_Term
+    # 1. remove 'obsolete' from label
+    # 2. remove 'OBSOLETE' from definition
+    # 3. update comment to "Note that this term was reinstated from obsolete"
+    #   TODO: no we remove the previous comment?  (all comments?)
+    # 4. Remove any replaced_by and consider tags
+    # 5. Remove the owl:deprecated: true tag
+
+    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>  "
+    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
+    prefix += "PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> "
+    prefix += "PREFIX obo: <http://purl.obolibrary.org/obo/> "
+    prefix += "PREFIX oboInOwl: <http://www.geneontology.org/formats/oboInOwl#> "
+
+    deleteQuery = about + " rdfs:label ?label . "
+    deleteQuery += about + ' owl:deprecated "true"^^xsd:boolean . '
+    deleteQuery += about + " obo:IAO_0000115 ?unobsolete_definition . "
+    deleteQuery += about + " obo:IAO_0100001 ?replacedBy .  "
+    deleteQuery += about + " oboInOwl:consider ?consider . "
+    deleteQuery += about + " rdfs:subClassOf oboInOwl:ObsoleteClass . "
+
+    delete = "DELETE {" + deleteQuery + "}"
+
+    insertQuery = about + " rdfs:label ?unobsolete_label . "
+    insertQuery += about + " obo:IAO_0000115 ?unobsolete_definition . "
+    insertQuery += (
+        '?entity rdfs:comment "Note that this term was reinstated from obsolete." . '
+    )
+
+    insert = "INSERT {" + insertQuery + "}"
+
+    whereQuery = "{ " + about + " rdfs:label ?label . "
+    whereQuery += 'BIND(IF(STRSTARTS(?label, "obsolete "),'
+    whereQuery += "SUBSTR(?label,10),?label) AS ?unobsolete_label ) } "
+    whereQuery += " UNION "
+    whereQuery += "{ " + about + " rdfs:label ?label . "
+    whereQuery += about + " obo:IAO_0000115 ?definition . "
+    whereQuery += 'BIND(IF(STRSTARTS(?definition, "OBSOLETE "),'
+    whereQuery += "SUBSTR(?definition,10),?definition) AS ?unobsolete_definition ) } "
+    whereQuery += " UNION "
+    whereQuery += "{ " + about + " rdfs:label ?label . "
+    whereQuery += about + " obo:IAO_0100001 ?replacedBy . } "
+    whereQuery += " UNION "
+    whereQuery += "{ " + about + " rdfs:label ?label . "
+    whereQuery += about + " oboInOwl:consider ?consider . } "
+
+    where = "WHERE {" + whereQuery + "}"
+
+    updateQuery = prefix + " " + delete + " " + insert + " " + where
+
+    return updateQuery
+
+
+def unobsolete_by_label(kgcl_instance):
+    about = kgcl_instance.about_node
+    # http://wiki.geneontology.org/index.php/Restoring_an_Obsolete_Ontology_Term
+    # 1. remove 'obsolete' from label
+    # 2. remove 'OBSOLETE' from definition
+    # 3. update comment to "Note that this term was reinstated from obsolete"
+    #   TODO: no we remove the previous comment?  (all comments?)
+    # 4. Remove any replaced_by and consider tags
+    # 5. Remove the owl:deprecated: true tag
+
+    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>  "
+    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
+    prefix += "PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> "
+    prefix += "PREFIX obo: <http://purl.obolibrary.org/obo/> "
+    prefix += "PREFIX oboInOwl: <http://www.geneontology.org/formats/oboInOwl#> "
+
+    deleteQuery = " ?about rdfs:label ?label . "
+    deleteQuery += ' ?about owl:deprecated "true"^^xsd:boolean . '
+    deleteQuery += " ?about obo:IAO_0000115 ?unobsolete_definition . "
+    deleteQuery += " ?about obo:IAO_0100001 ?replacedBy .  "
+    deleteQuery += " ?about oboInOwl:consider ?consider . "
+    deleteQuery += " ?about rdfs:subClassOf oboInOwl:ObsoleteClass . "
+
+    delete = "DELETE {" + deleteQuery + "}"
+
+    insertQuery = "?about rdfs:label ?unobsolete_label . "
+    insertQuery += "?about obo:IAO_0000115 ?unobsolete_definition . "
+    insertQuery += (
+        '?entity rdfs:comment "Note that this term was reinstated from obsolete." . '
+    )
+
+    insert = "INSERT {" + insertQuery + "}"
+
+    whereQuery = "{ ?about rdfs:label ?label . "
+    whereQuery += ' FILTER(STR(?label)="' + about + '") '
+    whereQuery += 'BIND(IF(STRSTARTS(?label, "obsolete "),'
+    whereQuery += "SUBSTR(?label,10),?label) AS ?unobsolete_label ) } "
+    whereQuery += " UNION "
+    whereQuery += "{ ?about rdfs:label ?label . "
+    whereQuery += ' FILTER(STR(?label)="' + about + '") '
+    whereQuery += "?about obo:IAO_0000115 ?definition . "
+    whereQuery += 'BIND(IF(STRSTARTS(?definition, "OBSOLETE "),'
+    whereQuery += "SUBSTR(?definition,10),?definition) AS ?unobsolete_definition ) } "
+    whereQuery += " UNION "
+    whereQuery += "{ ?about rdfs:label ?label . "
+    whereQuery += ' FILTER(STR(?label)="' + about + '") '
+    whereQuery += "?about obo:IAO_0100001 ?replacedBy . } "
+    whereQuery += " UNION "
+    whereQuery += "{ ?about rdfs:label ?label . "
+    whereQuery += ' FILTER(STR(?label)="' + about + '") '
+    whereQuery += "?about oboInOwl:consider ?consider . } "
+
+    where = "WHERE {" + whereQuery + "}"
+
+    updateQuery = prefix + " " + delete + " " + insert + " " + where
+
+    return updateQuery
+
+
+def unobsolete_curie(kgcl_instance):
+    about = kgcl_instance.about_node
+    # http://wiki.geneontology.org/index.php/Restoring_an_Obsolete_Ontology_Term
+    # 1. remove 'obsolete' from label
+    # 2. remove 'OBSOLETE' from definition
+    # 3. update comment to "Note that this term was reinstated from obsolete"
+    #   TODO: no we remove the previous comment?  (all comments?)
+    # 4. Remove any replaced_by and consider tags
+    # 5. Remove the owl:deprecated: true tag
+
+    curie_prefix = get_prefix(about)
+    curie_uri = prefix_2_uri[curie_prefix]
+
+    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>  "
+    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
+    prefix += "PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> "
+    prefix += "PREFIX obo: <http://purl.obolibrary.org/obo/> "
+    prefix += "PREFIX oboInOwl: <http://www.geneontology.org/formats/oboInOwl#> "
+    prefix += "PREFIX " + curie_prefix + ": " + curie_uri + " "
+
+    deleteQuery = about + " rdfs:label ?label . "
+    deleteQuery += about + ' owl:deprecated "true"^^xsd:boolean . '
+    deleteQuery += about + " obo:IAO_0000115 ?unobsolete_definition . "
+    deleteQuery += about + " obo:IAO_0100001 ?replacedBy .  "
+    deleteQuery += about + " oboInOwl:consider ?consider . "
+    deleteQuery += about + " rdfs:subClassOf oboInOwl:ObsoleteClass . "
+
+    delete = "DELETE {" + deleteQuery + "}"
+
+    insertQuery = about + " rdfs:label ?unobsolete_label . "
+    insertQuery += about + " obo:IAO_0000115 ?unobsolete_definition . "
+    insertQuery += (
+        '?entity rdfs:comment "Note that this term was reinstated from obsolete." . '
+    )
+
+    insert = "INSERT {" + insertQuery + "}"
+
+    whereQuery = "{ " + about + " rdfs:label ?label . "
+    whereQuery += 'BIND(IF(STRSTARTS(?label, "obsolete "),'
+    whereQuery += "SUBSTR(?label,10),?label) AS ?unobsolete_label ) } "
+    whereQuery += " UNION "
+    whereQuery += "{ " + about + " rdfs:label ?label . "
+    whereQuery += about + " obo:IAO_0000115 ?definition . "
+    whereQuery += 'BIND(IF(STRSTARTS(?definition, "OBSOLETE "),'
+    whereQuery += "SUBSTR(?definition,10),?definition) AS ?unobsolete_definition ) } "
+    whereQuery += " UNION "
+    whereQuery += "{ " + about + " rdfs:label ?label . "
+    whereQuery += about + " obo:IAO_0100001 ?replacedBy . } "
+    whereQuery += " UNION "
+    whereQuery += "{ " + about + " rdfs:label ?label . "
+    whereQuery += about + " oboInOwl:consider ?consider . } "
+
+    where = "WHERE {" + whereQuery + "}"
+
+    updateQuery = prefix + " " + delete + " " + insert + " " + where
+
+    return updateQuery
+
+
+# NB this does not preserve language tags
+def rename(kgcl_instance):
+    oldValue = kgcl_instance.old_value
+    newValue = kgcl_instance.new_value
+
+    # strip label's single quotes
+    oldValue = oldValue[1:-1]
+    newValue = newValue[1:-1]
+
+    old_language = kgcl_instance.old_language
+    new_language = kgcl_instance.new_language
+
+    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>"
+
+    if kgcl_instance.about_node is None:
+        subject = "?entity"
+    else:
+        subject = kgcl_instance.about_node
+        if kgcl_instance.about_node_representation == "curie":
+            prefix += build_curie_prefix(subject)
+
+    deleteQuery = subject + " rdfs:label ?oldlabel ."
+    delete = "DELETE {" + deleteQuery + "}"
+
+    insertQuery = subject + " rdfs:label ?newlabel ."
+    insert = "INSERT {" + insertQuery + "}"
+
+    whereQuery = subject + " rdfs:label ?label .  "
+    whereQuery += ' FILTER(STR(?label)="' + oldValue + '") '
+
+    if old_language is None:
+        whereQuery += ' BIND("' + oldValue + '" AS ?oldlabel) '
+    else:
+        whereQuery += ' FILTER(LANG(?label) ="' + old_language + '")'
+        whereQuery += (
+            ' BIND( STRLANG("' + oldValue + '","' + old_language + '") AS ?oldlabel) '
+        )
+
+    if new_language is None:
+        whereQuery += ' BIND("' + newValue + '" AS ?newlabel) '
+    else:
+        whereQuery += (
+            ' BIND( STRLANG("' + newValue + '","' + new_language + '") AS ?newlabel) '
+        )
+
+    where = "WHERE {" + whereQuery + "}"
+
+    updateQuery = prefix + " " + delete + " " + insert + " " + where
+
+    return updateQuery
+
+
+# TODO: this implementation is buggy
+# this implementation should preserve language tags
+# note that this cannot be used for diffing
+def rename_preserve(kgcl_instance):
+    oldValue = kgcl_instance.old_value
+    newValue = kgcl_instance.new_value
+    oldValue = oldValue.replace("'", "")
+    old_language = kgcl_instance.old_language
+    new_language = kgcl_instance.new_language
+
+    # initialise subject
+    if kgcl_instance.about_node is None:
+        subject = "?entity"
+    else:
+        subject = kgcl_instance.about_node
+
+    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>"
+    deleteQuery = subject + " rdfs:label ?tag ."
+    delete = "DELETE {" + deleteQuery + "}"
+
+    insertQuery = subject + " rdfs:label ?tag ."
+    insert = "INSERT {" + insertQuery + "}"
+
+    whereQuery = subject + " rdfs:label ?label .  "
+    whereQuery += " BIND( LANG(?label) AS ?language)  "
+    whereQuery += ' FILTER(STR(?label)="' + oldValue + '") '
+
+    # if old_Language tag is specified
+    # then, we need to filter results according to the specified language tag
+    if old_language is not None:
+        whereQuery += ' FILTER(LANG(?label) ="' + old_language + '")'
+
+    # if new_language tag is specifed, then
+    # we need to add this tag to insert query
+    if new_language is None:
+        whereQuery += " BIND( STRLANG(" + newValue + ",?language) AS ?tag) "
+    else:
+        whereQuery += (
+            " BIND( STRLANG(" + newValue + ',"' + new_language + '") AS ?tag) '
+        )
+
+    where = "WHERE {" + whereQuery + "}"
+
+    updateQuery = prefix + " " + delete + " " + insert + " " + where
+
+    return updateQuery
+
+
+def delete_by_id(kgcl_instance):
+    about = kgcl_instance.about_node  # this needs to be an ID - not a label
+
+    deleteQuery = (
+        "?s1 ?p1 " + about + " . "
+    )  # this does not delete triples with blank nodes
+    deleteQuery += "?s2 " + about + " ?o1 . "
+    deleteQuery += "?s2 " + about + " ?o1 . "
+    deleteQuery += about + " ?p2 ?o2 . "
+
+    delete = "DELETE {" + deleteQuery + "}"
+
+    whereQuery = "{ ?s1 ?p1 " + about + " . } "
+    whereQuery += " UNION "
+    whereQuery = "{ ?s2 " + about + " ?o1 . } "
+    whereQuery += " UNION "
+    whereQuery += "{ " + about + " ?p2 ?o2 . } "
+
+    where = "WHERE {" + whereQuery + "}"
+
+    updateQuery = delete + " " + where
+
+    return updateQuery
+
+
+def delete_curie(kgcl_instance):
+    about = kgcl_instance.about_node
+
+    prefix = build_curie_prefix(about)
+
+    # this does not delete triples with blank nodes
+    deleteQuery = "?s1 ?p1 " + about + " . "
+    deleteQuery += "?s2 " + about + " ?o1 . "
+    deleteQuery += about + " ?p2 ?o2 . "
+
+    delete = "DELETE {" + deleteQuery + "}"
+
+    whereQuery = "{ ?s1 ?p1 " + about + " . } "
+    whereQuery += " UNION "
+    whereQuery = "{ ?s2 " + about + " ?o1 . } "
+    whereQuery += " UNION "
+    whereQuery += "{ " + about + " ?p2 ?o2 . } "
+
+    where = "WHERE {" + whereQuery + "}"
+
+    updateQuery = prefix + " " + delete + " " + where
+
+    return updateQuery
+
+
+def delete_by_label(kgcl_instance):
+    about = kgcl_instance.about_node
+
+    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>"
+
+    deleteQuery = "?s1 rdfs:label ?label . "
+    deleteQuery += "?s1 ?p2 ?o1 . "
+
+    delete = "DELETE {" + deleteQuery + "}"
+
+    whereQuery = "?s1 rdfs:label ?label . "
+    whereQuery += "?s1 ?p2 ?o1 . "
+    whereQuery += ' FILTER(STR(?label)="' + about + '") '  # ignore language tags
+
+    where = "WHERE {" + whereQuery + "}"
+
+    updateQuery = prefix + " " + delete + " " + where
+
+    return updateQuery
+
+
+def create_class(kgcl_instance):
+    termId = kgcl_instance.node_id
+    id_type = kgcl_instance.about_node_representation
+
+    prefix = "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>  "
+    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
+
+    if id_type == "curie":
+        prefix += build_curie_prefix(termId)
+
+    insertQuery = termId + " rdf:type owl:Class  . "
+    insert = "INSERT {" + insertQuery + "}"
+    where = "WHERE {}"
+
+    updateQuery = prefix + " " + insert + " " + where
+
+    return updateQuery
+
+
+def create_node(kgcl_instance):
+    termId = kgcl_instance.node_id
+    label = kgcl_instance.name
+    language = kgcl_instance.language
+    id_type = kgcl_instance.about_node_representation
+
+    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>  "
+
+    if id_type == "curie":
+        prefix += build_curie_prefix(termId)
+
+    if language is None:
+        insertQuery = termId + " rdfs:label " + label + "  . "
+    else:
+        insertQuery = termId + " rdfs:label ?tag  . "
+
+    insert = "INSERT {" + insertQuery + "}"
+
+    if language is None:
+        where = "WHERE {}"
+    else:
+        whereQuery = " BIND( STRLANG(" + label + ',"' + language + '") AS ?tag) '
+        where = "WHERE {" + whereQuery + "}"
+
+    updateQuery = prefix + " " + insert + " " + where
+
+    return updateQuery
+
+
+def node_annotation_change(kgcl_instance):
+    subject = kgcl_instance.about_node
+    predicate = kgcl_instance.annotation_property
+    old_object = kgcl_instance.old_value
+    new_object = kgcl_instance.new_value
+
+    subject_type = kgcl_instance.about_node_representation
+    predicate_type = kgcl_instance.annotation_property_type
+    old_type = kgcl_instance.old_value_type
+    new_type = kgcl_instance.new_value_type
+
+    # TODO: allow language tags and datattypes for
+    # both the old and new annotation value
+    old_language = kgcl_instance.old_language
+    new_language = kgcl_instance.new_language
+    old_datatype = kgcl_instance.old_datatype
+    new_datatype = kgcl_instance.new_datatype
+
+    prefix = "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>  "
+    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
+
+    if subject_type == "curie":
+        prefix += build_curie_prefix(subject)
+
+    if predicate_type == "curie":
+        prefix += build_curie_prefix(predicate)
+
+    if old_type == "curie":
+        prefix += build_curie_prefix(old_object)
+
+    if new_type == "curie":
+        prefix += build_curie_prefix(new_object)
+
+        # todo: delete query
+    deleteQuery = "?subject ?predicate ?old_object . "
+
+    delete = "DELETE {" + deleteQuery + "}"
+
+    insertQuery = "?subject ?predicate ?new_object . "
+
+    insert = "INSERT {" + insertQuery + "}"
+
+    whereQuery = ""
+    if subject_type == "label":
+        whereQuery += "?subject rdfs:label ?subject_label . "
+        whereQuery += ' FILTER(STR(?subject_label)="' + subject + '") '
+    else:
+        whereQuery += " BIND(" + subject + " AS ?subject)"
+
+    if predicate_type == "label":
+        whereQuery += "?predicate rdfs:label ?predicate_label . "
+        whereQuery += ' FILTER(STR(?predicate_label)="' + predicate + '") '
+    else:
+        whereQuery += "?predicate rdf:type owl:AnnotationProperty . "
+        whereQuery += " BIND(" + predicate + " AS ?predicate)"
+
+    if old_type == "label":
+        whereQuery += "?old_object rdfs:label ?old_object_label . "
+        whereQuery += ' FILTER(STR(?old_object_label)="' + old_object + '") '
+
+    if old_type == "uri" or old_type == "curie":
+        whereQuery += " BIND(" + old_object + " AS ?old_object) "
+
+    if old_type == "literal":
+        old_object = escape_literal(old_object)
+        if old_datatype is not None:
+            whereQuery += (
+                ' BIND(STRDT("'
+                + old_object
+                + '",'
+                + old_datatype
+                + ") AS ?old_object) "
+            )
+        elif old_language is not None:
+            whereQuery += (
+                ' BIND(STRLANG("'
+                + old_object
+                + '","'
+                + old_language
+                + '") AS ?old_object) '
+            )
+        else:
+            whereQuery += ' BIND("' + old_object + '" AS ?old_object)'
+
+    if new_type == "label":
+        whereQuery += "?new_object rdfs:label ?new_object_label . "
+        whereQuery += ' FILTER(STR(?new_object_label)="' + new_object + '") '
+
+    if new_type == "uri" or old_type == "curie":
+        whereQuery += " BIND(" + new_object + " AS ?new_object) "
+
+    if new_type == "literal":
+        new_object = escape_literal(new_object)
+        if new_datatype is not None:
+            whereQuery += (
+                ' BIND(STRDT("'
+                + new_object
+                + '",'
+                + new_datatype
+                + ") AS ?new_object) "
+            )
+        elif new_language is not None:
+            whereQuery += (
+                ' BIND(STRLANG("'
+                + new_object
+                + '","'
+                + new_language
+                + '") AS ?new_object) '
+            )
+        else:
+            whereQuery += ' BIND("' + new_object + '" AS ?new_object)'
+
+    where = "WHERE { " + whereQuery + " }"
+
+    updateQuery = prefix + " " + delete + " " + insert + " " + where
+
+    return updateQuery
+
+
+def edge_annotation_creation(kgcl_instance):
+    subject = kgcl_instance.subject
+    predicate = kgcl_instance.predicate
+    object = kgcl_instance.object
+
+    subject_type = kgcl_instance.subject_type
+    predicate_type = kgcl_instance.predicate_type
+    object_type = kgcl_instance.object_type
+
+    annotation = kgcl_instance.annotation_set
+
+    # annotation.property_type
+    # annotation.filler_type
+
+    prefix = "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>  "
+    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
+
+    if subject_type == "curie":
+        prefix += build_curie_prefix(subject)
+
+    if predicate_type == "curie":
+        prefix += build_curie_prefix(predicate)
+
+    if object_type == "curie":
+        prefix += build_curie_prefix(object)
+
+    if annotation.property_type == "curie":
+        prefix += build_curie_prefix(annotation.property)
+
+    if annotation.filler_type == "curie":
+        prefix += build_curie_prefix(annotation.filler)
+
+    insertQuery = "?bnode owl:annotatedSource ?subject . "
+    insertQuery += "?bnode owl:annotatedProperty ?predicate . "
+    insertQuery += "?bnode owl:annotatedTarget ?object . "
+    insertQuery += "?bnode ?annotation_property ?filler . "
+    insertQuery += "?bnode rdf:type owl:Axiom ."
+    insert = "INSERT {" + insertQuery + "}"
+
+    whereQuery = ' BIND(BNODE("reification") AS ?bnode) '
+    if subject_type == "label":
+        whereQuery += "?subject rdfs:label ?subject_label . "
+        whereQuery += ' FILTER(STR(?subject_label)="' + subject + '") '
+    else:
+        whereQuery += "BIND(" + subject + " AS ?subject) "
+
+    if predicate_type == "label":
+        whereQuery += "?predicate rdfs:label ?predicate_label . "
+        whereQuery += ' FILTER(STR(?predicate_label)="' + predicate + '") '
+    else:
+        whereQuery += "BIND(" + predicate + " AS ?predicate) "
+
+    if annotation.property_type == "label":
+        whereQuery += "?annotation_property rdfs:label ?ap_label . "
+        whereQuery += ' FILTER(STR(?ap_label)="' + annotation.property + '") '
+    else:
+        whereQuery += "BIND(" + annotation.property + " AS ?annotation_property) "
+
+    if annotation.filler_type == "label":
+        whereQuery += "?filler rdfs:label ?filler_label . "
+        whereQuery += ' FILTER(STR(?filler_label)="' + annotation.filler + '") '
+
+    if annotation.filler_type == "literal":
+        whereQuery += 'BIND("' + escape_literal(annotation.filler) + '" AS ?filler) '
+
+    if annotation.filler_type == "uri" or annotation.filler_type == "curie":
+        whereQuery += "BIND(" + annotation.filler + " AS ?filler) "
+
+    if object_type == "label":
+        whereQuery += "?object rdfs:label ?object_label . "
+        whereQuery += ' FILTER(STR(?object_label)="' + object + '") '
+
+    if object_type == "literal":
+        whereQuery += 'BIND("' + escape_literal(object) + '" AS ?object) '
+
+    if object_type == "uri" or object_type == "curie":
+        whereQuery += "BIND(" + object + " AS ?object) "
+
+    where = "WHERE {" + whereQuery + "}"
+
+    updateQuery = prefix + " " + insert + " " + where
+
+    return updateQuery
+
+
+def triple_creation(kgcl_instance):
+    subject = kgcl_instance.subject
+    predicate = kgcl_instance.predicate
+    object = kgcl_instance.object
+
+    subject_type = kgcl_instance.subject_type
+    predicate_type = kgcl_instance.predicate_type
+    object_type = kgcl_instance.object_type
+
+    language = kgcl_instance.language
+    datatype = kgcl_instance.datatype  # TODO: currently only accepting full IRIs
+
+    prefix = "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>  "
+    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
+
+    if subject_type == "curie":
+        prefix += build_curie_prefix(subject)
+
+    if predicate_type == "curie":
+        prefix += build_curie_prefix(predicate)
+
+    if object_type == "curie":
+        prefix += build_curie_prefix(object)
+
+    insertQuery = "?subject ?predicate ?object . "
+
+    insert = "INSERT {" + insertQuery + "}"
+
+    whereQuery = ""
+    if subject_type == "label":
+        whereQuery += "?subject rdfs:label ?subject_label . "
+        whereQuery += ' FILTER(STR(?subject_label)="' + subject + '") '
+    else:
+        whereQuery += " BIND(" + subject + " AS ?subject)"
+
+    if predicate_type == "label":
+        whereQuery += "?predicate rdfs:label ?predicate_label . "
+        whereQuery += ' FILTER(STR(?predicate_label)="' + predicate + '") '
+    else:
+        whereQuery += " BIND(" + predicate + " AS ?predicate)"
+
+    if object_type == "label":
+        whereQuery += "?object rdfs:label ?object_label . "
+        whereQuery += ' FILTER(STR(?object_label)="' + object + '") '
+
+    if object_type == "uri" or object_type == "curie":
+        whereQuery += " BIND(" + object + " AS ?object) "
+
+    if object_type == "literal":
+        object = escape_literal(object)
+        if datatype is not None:
+            whereQuery += ' BIND(STRDT("' + object + '",' + datatype + ") AS ?object) "
+        elif language is not None:
+            whereQuery += (
+                ' BIND(STRLANG("' + object + '","' + language + '") AS ?object) '
+            )
+        else:
+            whereQuery += ' BIND("' + object + '" AS ?object)'
+
+    where = "WHERE { " + whereQuery + " }"
+
+    updateQuery = prefix + " " + insert + " " + where
+
+    return updateQuery
+
+
+# TODO: language tags + data types
+def edge_annotation_deletion(kgcl_instance):
+    subject = kgcl_instance.subject
+    predicate = kgcl_instance.predicate
+    object = kgcl_instance.object
+
+    subject_type = kgcl_instance.subject_type
+    predicate_type = kgcl_instance.predicate_type
+    object_type = kgcl_instance.object_type
+
+    annotation = kgcl_instance.annotation_set
+
+    # NB: we need to distinguish between two cases
+    # (a) the entire reification needs to be deleted
+    # (b) only one annotation triple is deleted
+    #     while the reification is not because it's
+    #     needed for other annotations
+
+    # TODO: example
+
+    # Query for (a)
+    # A reification for a single annotations will consist of 5 triples.
+    # So, we use this size bound to determine whether we need to
+    # delete the entire reification or not
+    prefix = "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>  "
+    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
+
+    if subject_type == "curie":
+        prefix += build_curie_prefix(subject)
+
+    if predicate_type == "curie":
+        prefix += build_curie_prefix(predicate)
+
+    if object_type == "curie":
+        prefix += build_curie_prefix(object)
+
+    if annotation.property_type == "curie":
+        prefix += build_curie_prefix(annotation.property)
+
+    if annotation.filler_type == "curie":
+        prefix += build_curie_prefix(annotation.filler)
+
+    insert = "INSERT { }"
+
+    deleteQuery = "?bnode owl:annotatedSource ?subject  . "
+    deleteQuery += "?bnode owl:annotatedProperty ?predicate . "
+    deleteQuery += "?bnode owl:annotatedTarget ?object . "
+    deleteQuery += "?bnode ?annotation_property ?filler . "
+    deleteQuery += "?bnode rdf:type owl:Axiom ."
+
+    delete = "DELETE {" + deleteQuery + "}"
+
+    whereQuery = "SELECT ?bnode ?subject ?predicate ?object ?filler ?annotation_property WHERE { "
+    whereQuery += "?bnode ?ap ?p . "
+    whereQuery += "?bnode owl:annotatedSource ?subject .  "
+
+    if subject_type == "label":
+        whereQuery += "?subject rdfs:label ?subject_label . "
+        whereQuery += ' FILTER(STR(?subject_label)="' + subject + '") '
+    else:
+        whereQuery += "BIND(" + subject + " AS ?subject) "
+
+    if annotation.property_type == "label":
+        whereQuery += "?annotation_property rdfs:label ?ap_label . "
+        whereQuery += ' FILTER(STR(?ap_label)="' + annotation.property + '") '
+    else:
+        whereQuery += "BIND(" + annotation.property + " AS ?annotation_property) "
+
+    if predicate_type == "label":
+        whereQuery += "?predicate rdfs:label ?predicate_label . "
+        whereQuery += ' FILTER(STR(?predicate_label)="' + predicate + '") '
+    else:
+        whereQuery += "BIND(" + predicate + " AS ?predicate) "
+
+    if annotation.filler_type == "label":
+        whereQuery += "?filler rdfs:label ?filler_label . "
+        whereQuery += ' FILTER(STR(?filler_label)="' + annotation.filler + '") '
+
+    if annotation.filler_type == "uri" or annotation.filler_type == "curie":
+        whereQuery += "BIND(" + annotation.filler + " AS ?filler) "
+
+    if annotation.filler_type == "literal":
+        whereQuery += 'BIND("' + escape_literal(annotation.filler) + '" AS ?filler) '
+
+    if object_type == "label":
+        whereQuery += "?object rdfs:label ?object_label . "
+        whereQuery += ' FILTER(STR(?object_label)="' + object + '") '
+
+    if object_type == "literal":
+        whereQuery += 'BIND("' + escape_literal(object) + '" AS ?object) '
+
+    if object_type == "uri" or object_type == "curie":
+        whereQuery += "BIND(" + object + " AS ?object)  "
+
+    whereQuery += "} "
+    whereQuery += "HAVING((COUNT(?ap) <= 5) && (COUNT(?p) <= 5)) "
+
+    where = "WHERE {" + whereQuery + "}"
+
+    subquery1 = prefix + " " + delete + " " + insert + " " + where
+
+    # Query for (b)
+    deleteQuery = "?bnode ?annotation_property ?filler . "
+    delete = "DELETE {" + deleteQuery + "}"
+
+    whereQuery = "SELECT ?bnode ?subject ?object ?annotation_property ?filler WHERE {"
+    whereQuery += "?bnode ?ap ?p . "
+    whereQuery += "?bnode owl:annotatedSource ?subject .  "
+
+    if subject_type == "label":
+        whereQuery += "?subject rdfs:label ?subject_label . "
+        whereQuery += ' FILTER(STR(?subject_label)="' + subject + '") '
+    else:
+        whereQuery += "BIND(" + subject + " AS ?subject) "
+
+    if annotation.property_type == "label":
+        whereQuery += "?annotation_property rdfs:label ?ap_label . "
+        whereQuery += ' FILTER(STR(?ap_label)="' + annotation.property + '") '
+    else:
+        whereQuery += "BIND(" + annotation.property + " AS ?annotation_property) "
+
+    if annotation.filler_type == "label":
+        whereQuery += "?filler rdfs:label ?filler_label . "
+        whereQuery += ' FILTER(STR(?filler_label)="' + annotation.filler + '") '
+
+    if annotation.filler_type == "uri" or annotation.filler_type == "curie":
+        whereQuery += "BIND(" + annotation.filler + " AS ?filler) "
+    if annotation.filler_type == "literal":
+        whereQuery += 'BIND("' + escape_literal(annotation.filler) + '" AS ?filler) '
+
+    if object_type == "label":
+        whereQuery += "?object rdfs:label ?object_label . "
+        whereQuery += ' FILTER(STR(?object_label)="' + object + '") '
+
+    if object_type == "literal":
+        whereQuery += 'BIND("' + escape_literal(object) + '" AS ?object) '
+
+    if object_type == "uri" or object_type == "curie":
+        whereQuery += "BIND(" + object + " AS ?object)  "
+
+    whereQuery += "} "
+    whereQuery += "HAVING((COUNT(?ap) > 5) || (COUNT(?ap) > 5)) "
+
+    where = "WHERE {" + whereQuery + "}"
+
+    subquery2 = prefix + " " + delete + " " + insert + " " + where
+
+    # putting (a) and (b) together
+    updateQuery = subquery1 + " ; " + subquery2
+
+    return updateQuery
+
+
+def triple_deletion(kgcl_instance):
+    subject = kgcl_instance.subject
+    predicate = kgcl_instance.predicate
+    object = kgcl_instance.object
+
+    subject_type = kgcl_instance.subject_type
+    predicate_type = kgcl_instance.predicate_type
+    object_type = kgcl_instance.object_type
+
+    language = kgcl_instance.language
+    datatype = kgcl_instance.datatype
+
+    prefix = "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>  "
+    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
+
+    if subject_type == "curie":
+        prefix += build_curie_prefix(subject)
+
+    if predicate_type == "curie":
+        prefix += build_curie_prefix(predicate)
+
+    if object_type == "curie":
+        prefix += build_curie_prefix(object)
+
+    deleteQuery = "?subject ?predicate ?object . "
+    delete = "DELETE {" + deleteQuery + "}"
+
+    whereQuery = ""
+
+    whereQuery = ""
+    if subject_type == "label":
+        whereQuery += "?subject rdfs:label ?object_label . "
+        whereQuery += ' FILTER(STR(?object_label)="' + subject + '") '
+    else:
+        whereQuery += "BIND(" + subject + " AS ?subject)"
+
+    if predicate_type == "label":
+        whereQuery += "?predicate rdfs:label ?predicate_label . "
+        whereQuery += ' FILTER(STR(?predicate_label)="' + predicate + '") '
+    else:
+        whereQuery += "BIND(" + predicate + " AS ?predicate)"
+
+    if object_type == "label":
+        whereQuery += "?object rdfs:label ?object_label . "
+        whereQuery += ' FILTER(STR(?object_label)="' + object + '") '
+
+    if object_type == "uri" or object_type == "curie":
+        whereQuery += " BIND(" + object + " AS ?object) "
+
+    if object_type == "literal":
+        object = escape_literal(object)
+        if datatype is not None:
+            whereQuery += ' BIND( STRDT("' + object + '",' + datatype + ") AS ?object) "
+        elif language is not None:
+            whereQuery += (
+                'BIND( STRLANG("' + object + '","' + language + '") AS ?object) '
+            )
+        else:
+            whereQuery += 'BIND("' + object + '" AS ?object)'
+
+    where = "WHERE { " + whereQuery + " }"
+
+    updateQuery = prefix + " " + delete + " " + where
+
+    return updateQuery
+
+
+def obsolete_by_id(kgcl_instance):
+    about = kgcl_instance.about_node
+    replacement = kgcl_instance.has_direct_replacement
+
+    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>  "
+    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
+    prefix += "PREFIX xsd:  <http://www.w3.org/2001/XMLSchema#> "
+    prefix += "PREFIX obo: <http://purl.obolibrary.org/obo/> "
+    prefix += "PREFIX oboInOwl: <http://www.geneontology.org/formats/oboInOwl#> "
+
+    deleteQuery = about + "rdfs:label ?label . "
+    deleteQuery += about + " rdfs:subClassOf ?superclass . "
+    deleteQuery += about + " owl:equivalentClass ?rhs . "
+    deleteQuery += "?lhs owl:equivalentClass " + about + " ."
+
+    delete = "DELETE {" + deleteQuery + "}"
+
+    insertQuery = "?entity rdfs:label ?tag . "
+    insertQuery += about + ' owl:deprecated "true"^^xsd:boolean . '
+    insertQuery += "?entity rdfs:subClassOf oboInOwl:ObsoleteClass . "
+
+    if kgcl_instance.has_direct_replacement is not None:
+        insertQuery += about + " obo:IAO_0100001 " + replacement + "  .  "
+
+    insert = "INSERT {" + insertQuery + "}"
+
+    whereQuery = "{ " + about + " rdfs:subClassOf ?superclass . } "
+    whereQuery += " UNION "
+    whereQuery += "{ " + about + " owl:equivalentClass ?rhs . } "
+    whereQuery += " UNION "
+    whereQuery += "{ ?lhs owl:equivalentClass " + about + " . } "
+    whereQuery += " UNION "
+    whereQuery += "{ ?entity rdfs:label ?label . "
+    whereQuery += ' BIND(CONCAT("obsolete ", ?label) AS ?obsolete_label )  '
+    whereQuery += " BIND( LANG(?label) AS ?language)  "
+    whereQuery += " BIND( STRLANG(?obsolete_label,?language) AS ?tag) }  "
+
+    where = "WHERE {" + whereQuery + "}"
+
+    updateQuery = prefix + " " + delete + " " + insert + " " + where
+
+    return updateQuery
+
+
+def obsolete_by_label(kgcl_instance):
+    about = kgcl_instance.about_node
+
+    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>  "
+    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
+    prefix += "PREFIX xsd:  <http://www.w3.org/2001/XMLSchema#> "
+    prefix += "PREFIX oboInOwl: <http://www.geneontology.org/formats/oboInOwl#> "
+
+    deleteQuery = "?entity rdfs:label ?label . "
+    deleteQuery += "?entity rdfs:subClassOf ?superclass . "
+    deleteQuery += "?entity owl:equivalentClass ?rhs . "
+    deleteQuery += "?lhs owl:equivalentClass ?entity . "
+
+    delete = "DELETE {" + deleteQuery + "}"
+
+    # inner_label = about.replace("'", "")
+    obsolete_label = "obsolete " + about
+
+    insertQuery = "?entity rdfs:label ?tag . "
+    insertQuery += '?entity owl:deprecated "true"^^xsd:boolean . '
+    insertQuery += "?entity rdfs:subClassOf oboInOwl:ObsoleteClass . "
+
+    insert = "INSERT {" + insertQuery + "}"
+
+    # TODO: handle the special case where only a label is present
+    # (in that case we need to query for a single triple)
+    whereQuery = "{ ?entity rdfs:label ?label .  "
+    whereQuery += " ?entity rdfs:subClassOf ?superclass .  "
+    whereQuery += " BIND( LANG(?label) AS ?language)  "
+    whereQuery += ' BIND( STRLANG("' + obsolete_label + '",?language) AS ?tag)  '
+    whereQuery += ' FILTER(STR(?label)="' + about + '") } '
+
+    whereQuery += " UNION "
+
+    whereQuery += "{ ?entity rdfs:label ?label .  "
+    whereQuery += " ?entity owl:equivalentClass ?rhs . "
+    whereQuery += " BIND( LANG(?label) AS ?language) "
+    whereQuery += ' BIND( STRLANG("' + obsolete_label + '",?language) AS ?tag)  '
+    whereQuery += ' FILTER(STR(?label)="' + about + '") } '
+
+    whereQuery += " UNION "
+
+    whereQuery += "{ ?entity rdfs:label ?label .  "
+    whereQuery += " ?lhs owl:equivalentClass ?entity . "
+    whereQuery += " BIND( LANG(?label) AS ?language) "
+    whereQuery += ' BIND( STRLANG("' + obsolete_label + '",?language) AS ?tag)  '
+    whereQuery += ' FILTER(STR(?label)="' + about + '") } '
+
+    whereQuery += " UNION "
+
+    whereQuery += "{ ?entity rdfs:label ?label .  "
+    whereQuery += " ?entity rdf:type ?type .  "
+    whereQuery += " BIND( LANG(?label) AS ?language) "
+    whereQuery += ' BIND( STRLANG("' + obsolete_label + '",?language) AS ?tag)  '
+    whereQuery += ' FILTER(STR(?label)="' + about + '") } '
+
+    where = "WHERE {" + whereQuery + "}"
+
+    updateQuery = prefix + " " + delete + " " + insert + " " + where
+
+    return updateQuery
+
+
+def obsolete_curie(kgcl_instance):
+    about = kgcl_instance.about_node
+    replacement = kgcl_instance.has_direct_replacement
+
+    curie_prefix = get_prefix(about)
+    curie_uri = prefix_2_uri[curie_prefix]
+
+    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>  "
+    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
+    prefix += "PREFIX xsd:  <http://www.w3.org/2001/XMLSchema#> "
+    prefix += "PREFIX obo: <http://purl.obolibrary.org/obo/> "
+    prefix += "PREFIX oboInOwl: <http://www.geneontology.org/formats/oboInOwl#> "
+    prefix += "PREFIX " + curie_prefix + ": " + curie_uri + " "
+
+    deleteQuery = about + " rdfs:label ?label . "
+    deleteQuery += about + " rdfs:subClassOf ?superclass . "
+    deleteQuery += about + " owl:equivalentClass ?rhs . "
+    deleteQuery += "?lhs owl:equivalentClass " + about + " ."
+
+    delete = "DELETE {" + deleteQuery + "}"
+
+    insertQuery = "?entity rdfs:label ?tag . "
+    insertQuery += about + ' owl:deprecated "true"^^xsd:boolean . '
+    insertQuery += "?entity rdfs:subClassOf oboInOwl:ObsoleteClass . "
+
+    if kgcl_instance.has_direct_replacement is not None:
+        insertQuery += about + " obo:IAO_0100001 " + replacement + "  .  "
+
+    insert = "INSERT {" + insertQuery + "}"
+
+    whereQuery = "{ " + about + " rdfs:subClassOf ?superclass . } "
+    whereQuery += " UNION "
+    whereQuery += "{ " + about + " owl:equivalentClass ?rhs . } "
+    whereQuery += " UNION "
+    whereQuery += "{ ?lhs owl:equivalentClass " + about + " . } "
+    whereQuery += " UNION "
+    whereQuery += "{ ?entity rdfs:label ?label . "
+    whereQuery += ' BIND(CONCAT("obsolete ", ?label) AS ?obsolete_label )  '
+    whereQuery += " BIND( LANG(?label) AS ?language)  "
+    whereQuery += " BIND( STRLANG(?obsolete_label,?language) AS ?tag) }  "
+
+    where = "WHERE {" + whereQuery + "}"
+
+    updateQuery = prefix + " " + delete + " " + insert + " " + where
+
+    return updateQuery
+
+
+def new_synonym_for_uri(kgcl_instance):
+
+    about = kgcl_instance.about_node
+    synonym = kgcl_instance.new_value
+    language = kgcl_instance.language
+    qualifier = kgcl_instance.qualifier
+
+    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>  "
+    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
+    prefix += "PREFIX xsd:  <http://www.w3.org/2001/XMLSchema#> "
+    prefix += "PREFIX oboInOwl: <http://www.geneontology.org/formats/oboInOwl#> "
+
+    if qualifier is None:
+        insertQuery = about + " oboInOwl:hasSynonym "  # + synonym + " . "
+    if qualifier == "exact":
+        insertQuery = about + " oboInOwl:hasExactSynonym "  # + synonym + " . "
+    if qualifier == "narrow":
+        insertQuery = about + " oboInOwl:hasNarrowSynonym "  # + synonym + " . "
+    if qualifier == "broad":
+        insertQuery = about + " oboInOwl:hasBroadSynonym "  # + synonym + " . "
+    if qualifier == "related":
+        insertQuery = about + " oboInOwl:hasRelatedSynonym "  # + synonym + " . "
+
+    whereQuery = ""
+    if language is None:
+        insertQuery += '"' + synonym + '" .'
+    else:
+        insertQuery += "?tag ."
+        whereQuery = ' BIND( STRLANG("' + synonym + '","' + language + '") AS ?tag) '
+
+    insert = "INSERT {" + insertQuery + "}"
+    where = "WHERE {" + whereQuery + "}"
+
+    updateQuery = prefix + " " + insert + " " + where
+
+    return updateQuery
+
+
+def new_synonym_for_label(kgcl_instance):
+    about = kgcl_instance.about_node  # this is a label for a node
+    synonym = kgcl_instance.new_value
+    language = kgcl_instance.language
+    qualifier = kgcl_instance.qualifier
+
+    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>  "
+    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
+    prefix += "PREFIX xsd:  <http://www.w3.org/2001/XMLSchema#> "
+    prefix += "PREFIX oboInOwl: <http://www.geneontology.org/formats/oboInOwl#> "
+
+    if qualifier is None:
+        insertQuery = "?about oboInOwl:hasSynonym "  # + synonym + " . "
+    if qualifier == "exact":
+        insertQuery = "?about oboInOwl:hasExactSynonym "  # + synonym + " . "
+    if qualifier == "narrow":
+        insertQuery = "?about oboInOwl:hasNarrowSynonym "  # + synonym + " . "
+    if qualifier == "broad":
+        insertQuery = "?about oboInOwl:hasBroadSynonym "  # + synonym + " . "
+    if qualifier == "related":
+        insertQuery = "?about oboInOwl:hasRelatedSynonym "  # + synonym + " . "
+
+    # this ignores language tags
+    whereQuery = " ?about rdfs:label ?label . "
+    whereQuery += ' FILTER(STR(?label)="' + about + '") '
+
+    if language is None:
+        insertQuery += '"' + synonym + '" .'
+    else:
+        insertQuery += "?tag ."
+        whereQuery += ' BIND( STRLANG("' + synonym + '","' + language + '") AS ?tag) '
+
+    insert = "INSERT {" + insertQuery + "}"
+    where = "WHERE {" + whereQuery + "}"
+
+    updateQuery = prefix + " " + insert + " " + where
+
+    return updateQuery
+
+
+def new_synonym_for_curie(kgcl_instance):
+    about = kgcl_instance.about_node  # this is a curie
+    synonym = kgcl_instance.new_value
+    language = kgcl_instance.language
+    qualifier = kgcl_instance.qualifier
+
+    curie_prefix = get_prefix(about)
+    curie_uri = prefix_2_uri[curie_prefix]
+
+    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>  "
+    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
+    prefix += "PREFIX xsd:  <http://www.w3.org/2001/XMLSchema#> "
+    prefix += "PREFIX oboInOwl: <http://www.geneontology.org/formats/oboInOwl#> "
+    prefix += "PREFIX " + curie_prefix + ": " + curie_uri + " "
+
+    if qualifier is None:
+        insertQuery = about + " oboInOwl:hasSynonym "  # + synonym + " . "
+    if qualifier == "exact":
+        insertQuery = about + " oboInOwl:hasExactSynonym "  # + synonym + " . "
+    if qualifier == "narrow":
+        insertQuery = about + " oboInOwl:hasNarrowSynonym "  # + synonym + " . "
+    if qualifier == "broad":
+        insertQuery = about + " oboInOwl:hasBroadSynonym "  # + synonym + " . "
+    if qualifier == "related":
+        insertQuery = about + " oboInOwl:hasRelatedSynonym "  # + synonym + " . "
+
+    whereQuery = ""
+    if language is None:
+        insertQuery += '"' + synonym + '" .'
+    else:
+        insertQuery += "?tag ."
+        whereQuery += ' BIND( STRLANG("' + synonym + '","' + language + '") AS ?tag) '
+
+    insert = "INSERT {" + insertQuery + "}"
+    where = "WHERE {" + whereQuery + "}"
+
+    updateQuery = prefix + " " + insert + " " + where
+
+    return updateQuery
+
+
+def create_existential_restriction(kgcl_instance):
+    subclass = kgcl_instance.subject
+    property = kgcl_instance.predicate
+    filler = kgcl_instance.object
+
+    subclass_type = kgcl_instance.subject_type
+    property_type = kgcl_instance.predicate_type
+    filler_type = kgcl_instance.object_type
+
+    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>  "
+    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
+    prefix += "PREFIX xsd:  <http://www.w3.org/2001/XMLSchema#> "
+
+    if subclass_type == "curie":
+        prefix += build_curie_prefix(subclass)
+    if property_type == "curie":
+        prefix += build_curie_prefix(property)
+    if filler_type == "curie":
+        prefix += build_curie_prefix(filler)
+
+    insertQuery = "?subclass rdfs:subClassOf ?bnode . "
+    insertQuery += "?bnode owl:someValuesFrom ?filler . "
+    insertQuery += "?bnode owl:onProperty ?property . "
+    insertQuery += "?bnode rdf:type owl:Restriction ."
+
+    insert = "INSERT {" + insertQuery + "}"
+
+    whereQuery = ""
+    if subclass_type == "label":
+        whereQuery += "?subclass rdfs:label ?subclass_label . "
+        whereQuery += ' FILTER(STR(?subclass_label)="' + subclass + '") '
+    else:
+        whereQuery += " BIND(" + subclass + " AS ?subclass) "
+
+    if property_type == "label":
+        whereQuery += "?property rdfs:label ?property_label . "
+        whereQuery += ' FILTER(STR(?property_label)="' + property + '") '
+    else:
+        whereQuery += " BIND(" + property + " AS ?property) "
+
+    if filler_type == "label":
+        whereQuery += "?filler rdfs:label ?filler_label . "
+        whereQuery += ' FILTER(STR(?filler_label)="' + filler + '") '
+    else:
+        whereQuery += " BIND(" + filler + " AS ?filler) "
+
+    whereQuery += ' BIND(BNODE("existential") AS ?bnode) '
+    where = "WHERE {" + whereQuery + "}"
+
+    updateQuery = prefix + " " + insert + " " + where
+
+    return updateQuery
+
+
+def delete_existential_restriction(kgcl_instance):
+    subclass = kgcl_instance.subject
+    property = kgcl_instance.predicate
+    filler = kgcl_instance.object
+
+    subclass_type = kgcl_instance.subject_type
+    property_type = kgcl_instance.predicate_type
+    filler_type = kgcl_instance.object_type
+
+    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>  "
+    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
+    prefix += "PREFIX xsd:  <http://www.w3.org/2001/XMLSchema#> "
+    prefix += "PREFIX oboInOwl: <http://www.geneontology.org/formats/oboInOwl#> "
+
+    if subclass_type == "curie":
+        prefix += build_curie_prefix(subclass)
+    if property_type == "curie":
+        prefix += build_curie_prefix(property)
+    if filler_type == "curie":
+        prefix += build_curie_prefix(filler)
+
+    deleteQuery = "?subclass rdfs:subClassOf ?bnode . "
+    deleteQuery += "?bnode owl:someValuesFrom ?filler . "
+    deleteQuery += "?bnode owl:onProperty ?property . "
+    deleteQuery += "?bnode rdf:type owl:Restriction ."
+
+    delete = "DELETE {" + deleteQuery + "}"
+
+    whereQuery = deleteQuery
+
+    if subclass_type == "label":
+        whereQuery += "?subclass rdfs:label ?subclass_label . "
+        whereQuery += ' FILTER(STR(?subclass_label)="' + subclass + '") '
+    else:
+        whereQuery += " BIND(" + subclass + " AS ?subclass) "
+
+    if property_type == "label":
+        whereQuery += "?property rdfs:label ?property_label . "
+        whereQuery += ' FILTER(STR(?property_label)="' + property + '") '
+    else:
+        whereQuery += " BIND(" + property + " AS ?property) "
+
+    if filler_type == "label":
+        whereQuery += "?filler rdfs:label ?filler_label . "
+        whereQuery += ' FILTER(STR(?filler_label)="' + filler + '") '
+    else:
+        whereQuery += " BIND(" + filler + " AS ?filler) "
+
+    where = "WHERE {" + whereQuery + "}"
+
+    updateQuery = prefix + " " + delete + " " + where
+
+    return updateQuery
diff --git a/kgcl/diff/__init__.py b/kgcl/diff/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/kgcl/diff/change_detection.py b/kgcl/diff/change_detection.py
new file mode 100644
index 00000000..5907b78d
--- /dev/null
+++ b/kgcl/diff/change_detection.py
@@ -0,0 +1,388 @@
+import rdflib
+from rdflib.namespace import RDFS
+from rdflib import URIRef, Literal
+from kgcl.model.kgcl import (
+    NodeRename,
+    NodeObsoletion,
+    NodeUnobsoletion,
+    NodeAnnotationChange,
+    NodeDeletion,
+    NodeMove,
+    NodeDeepening,
+    NodeShallowing,
+    EdgeCreation,
+    EdgeDeletion,
+    PredicateChange,
+    NodeCreation,
+    ClassCreation,
+    NewSynonym,
+)
+from kgcl.model.ontology_model import Edge
+
+
+def id_generator():
+    """
+    Returns a new ID for KGCL change operations.
+    """
+    id = 0
+    while True:
+        yield id
+        id += 1
+
+
+# initialise ID generator
+id_gen = id_generator()
+
+# TODO:
+# obsoletion
+# unobsoletion
+
+
+def get_type(rdf_entity):
+    if isinstance(rdf_entity, URIRef):
+        return "uri"
+    elif isinstance(rdf_entity, Literal):
+        return "literal"
+    else:
+        return "Error"
+
+
+def detect_renamings(added, deleted):
+    """Given a diff represented by 'added' and 'deleted' triples,
+    return an encoding in terms of
+    (i) NodeRename instances,
+    (ii) triples involved in (i), and
+    (iii) nondeterministic choices.
+    """
+
+    covered = rdflib.Graph()
+
+    # get labeling information
+    added_labels = {}
+    deleted_labels = {}
+
+    for s, p, o in added.triples((None, RDFS.label, None)):
+        if s not in added_labels:
+            added_labels[s] = set()
+        added_labels[s].add(o)
+
+    for s, p, o in deleted.triples((None, RDFS.label, None)):
+        if s not in deleted_labels:
+            deleted_labels[s] = set()
+        deleted_labels[s].add(o)
+
+    # collect renamings
+    nonDeterministic = []  # list of non-deterministic choices
+    kgcl = []
+    for subject in deleted_labels:
+        if subject in added_labels:
+
+            moved_from = deleted_labels[subject]
+            moved_to = added_labels[subject]
+
+            if len(moved_to) > 1 or len(moved_from) > 1:
+                nonDeterministic.append((set(moved_from), set(moved_to)))
+
+            shared = min(len(moved_to), len(moved_from))
+            for x in range(shared):
+                id = "test_id_" + str(next(id_gen))
+
+                new = moved_to.pop()
+                old = moved_from.pop()
+
+                old_label = str(old)
+                new_label = str(new)
+                # get language tags
+                old_language = old.language
+                new_language = new.language
+
+                node = NodeRename(
+                    id=id,
+                    about_node=subject,
+                    old_value=old_label,
+                    new_value=new_label,
+                    old_language=old_language,
+                    new_language=new_language,
+                )
+                kgcl.append(node)
+
+                covered.add((subject, RDFS.label, new))
+                covered.add((subject, RDFS.label, old))
+
+    return kgcl, covered, nonDeterministic
+
+
+def detect_annotation_changes(added, deleted, new_annotations, old_annotations):
+    covered = rdflib.Graph()
+
+    # maps from subjects to lists of annotation properties
+    s_2_aps_deleted = {}
+    s_2_aps_added = {}
+
+    kgcl = []
+    for s, p, o in added:
+        if p in new_annotations:
+            if s not in s_2_aps_added:
+                s_2_aps_added[s] = []
+            s_2_aps_added[s].append(p)
+
+    for s, p, o in deleted:
+        if p in old_annotations:
+            if s not in s_2_aps_deleted:
+                s_2_aps_deleted[s] = []
+            s_2_aps_deleted[s].append(p)
+
+    kgcl = []
+    nonDeterministic = []  # list of non-deterministic choices
+    for s in s_2_aps_added:
+        if s in s_2_aps_deleted:
+
+            # get properties as sets
+            added_properties = set(s_2_aps_added[s])
+            deleted_properties = set(s_2_aps_deleted[s])
+
+            # get intersection of relevant objects
+            shared_properties = added_properties & deleted_properties
+
+            for i in shared_properties:
+                # get corresponding predicates
+                changed_to = set()
+                changed_from = set()
+                for s, p, o in added.triples((s, i, None)):
+                    changed_to.add((s, p, o))
+                for s, p, o in deleted.triples((s, i, None)):
+                    changed_from.add((s, p, o))
+
+                if len(changed_to) > 1 or len(changed_from) > 1:
+                    nonDeterministic.append((set(changed_from), set(changed_to)))
+
+                # match potential annotation changes
+                m = min(len(changed_to), len(changed_from))
+
+                for x in range(m):
+                    id = "test_id_" + str(next(id_gen))
+                    old = (changed_from.pop())[2]
+                    new = (changed_to.pop())[2]
+
+                    old_language = None
+                    old_datatype = None
+                    if isinstance(old, Literal):
+                        old_language = old.language
+                        old_datatype = old.datatype
+                        if old_datatype is not None:
+                            old_datatype = (
+                                "<" + old_datatype + ">"
+                            )  # expect data types without curies
+
+                    new_language = None
+                    new_datatype = None
+                    if isinstance(new, Literal):
+                        new_language = new.language
+                        new_datatype = new.datatype
+                        if new_datatype is not None:
+                            new_datatype = (
+                                "<" + new_datatype + ">"
+                            )  # expect data types without curies
+
+                    change = NodeAnnotationChange(
+                        id=id,
+                        about_node=str(s),
+                        about_node_representation=get_type(s),
+                        annotation_property=str(i),
+                        annotation_property_type=get_type(i),
+                        old_value=str(old),
+                        new_value=str(new),
+                        old_value_type=get_type(old),
+                        new_value_type=get_type(new),
+                        old_language=old_language,
+                        new_language=new_language,
+                        old_datatype=old_datatype,
+                        new_datatype=new_datatype,
+                    )
+                    kgcl.append(change)
+                    covered.add((s, i, old))
+                    covered.add((s, i, new))
+
+    return kgcl, covered, nonDeterministic
+
+
+def detect_node_moves(added, deleted):
+    """Given a diff represented by 'added' and 'deleted' triples,
+    return an encoding in terms of
+    (i) NodeMove instances,
+    (ii) triples involved in (i), and
+    (iii) nondeterministic choices.
+    """
+    covered = rdflib.Graph()
+
+    # map from subject 2 property (in added)
+    s2p_added = {}
+    for s, p, o in added:
+        if s not in s2p_added:
+            s2p_added[s] = set()
+        s2p_added[s].add(p)
+
+    # map from subject 2 property (in deleted)
+    s2p_deleted = {}
+    for s, p, o in deleted:
+        if s not in s2p_deleted:
+            s2p_deleted[s] = set()
+        s2p_deleted[s].add(p)
+
+    # identify triples that only differ wrt their object
+    s2p_shared = {}
+    for s in s2p_added:
+        if s in s2p_deleted:
+            s2p_shared[s] = s2p_added[s] & s2p_deleted[s]
+
+    kgcl = []  # list of detected KGCL node modes
+    nonDeterministic = []  # list of non-deterministic choices
+    for subject in s2p_shared:
+        for predicate in s2p_shared[subject]:
+
+            # triples with the same subject and predicate
+            moved_to = set()
+            moved_from = set()
+            for s, p, o in added.triples((subject, predicate, None)):
+                if isinstance(o, URIRef):  # node move operates only on nodes
+                    moved_to.add((s, p, o))
+            for s, p, o in deleted.triples((subject, predicate, None)):
+                if isinstance(o, URIRef):
+                    moved_from.add((s, p, o))
+
+            if len(moved_to) > 1 or len(moved_from) > 1:
+                nonDeterministic.append((set(moved_from), set(moved_to)))
+
+            # TODO: impose an order to make this deterministic
+            shared = min(len(moved_to), len(moved_from))
+            for x in range(shared):
+                id = "test_id_" + str(next(id_gen))
+
+                new = moved_to.pop()
+                old = moved_from.pop()
+
+                covered.add(new)
+                covered.add(old)
+
+                old_subject = str(old[0])
+                old_predicate = str(old[1])
+                old_object = str(old[2])
+                new_object = str(new[2])
+
+                edge = Edge(
+                    subject=old_subject, predicate=old_predicate, object=old_object
+                )
+
+                # record entity type for KGCL rendering purposes
+                old_object_type = get_type(old[2])
+                new_object_type = get_type(new[2])
+
+                node = NodeMove(
+                    id=id,
+                    about_edge=edge,
+                    old_value=old_object,
+                    new_value=new_object,
+                    old_object_type=old_object_type,
+                    new_object_type=new_object_type,
+                )
+
+                kgcl.append(node)
+
+    return kgcl, covered, nonDeterministic
+
+
+def detect_predicate_changes(added, deleted):
+    """Given a diff represented by 'added' and 'deleted' triples,
+    return an encoding in terms of
+    (i) PredicateChange instances,
+    (ii) triples involved in (i), and
+    (iii) nondeterministic choices.
+    """
+    covered = rdflib.Graph()
+
+    # maps from subjects to lists of objects
+    s_2_os_deleted = {}
+    s_2_os_added = {}
+
+    for s, p, o in added:
+        if s not in s_2_os_added:
+            s_2_os_added[s] = []
+        s_2_os_added[s].append(o)
+
+    for s, p, o in deleted:
+        if s not in s_2_os_deleted:
+            s_2_os_deleted[s] = []
+        s_2_os_deleted[s].append(o)
+
+    # encode renamings wrt KGCL model
+    kgcl = []
+    nonDeterministic = []  # list of non-deterministic choices
+    for s in s_2_os_added:
+        if s in s_2_os_deleted:
+
+            # get the objects as sets
+            added_objects = set(s_2_os_added[s])
+            deleted_objects = set(s_2_os_deleted[s])
+
+            # get intersection of relevant objects
+            shared_objects = added_objects & deleted_objects
+
+            for i in shared_objects:
+                # get corresponding predicates
+                changed_to = set()
+                changed_from = set()
+                for s, p, o in added.triples((s, None, i)):
+                    changed_to.add((s, p, o))
+                for s, p, o in deleted.triples((s, None, i)):
+                    changed_from.add((s, p, o))
+
+                # create KGCL edge object
+                edge = Edge(subject=str(s), object=str(i))
+
+                language = None
+                datatype = None
+                if isinstance(i, Literal):
+                    language = i.language
+                    datatype = i.datatype
+                    if datatype is not None:
+                        datatype = (
+                            "<" + datatype + ">"
+                        )  # expect data types without curies
+
+                # if i.language is not None:
+                #    language = str(i.language)
+                # else:
+                #    language = None
+                # if i.datatype is not None:
+                #    datatype = str(i.datatype)
+                # else:
+                #    datatype = None
+
+                object_type = get_type(i)
+
+                if len(changed_to) > 1 or len(changed_from) > 1:
+                    nonDeterministic.append((set(changed_from), set(changed_to)))
+
+                # match potential predicate changes
+                m = min(len(changed_to), len(changed_from))
+                for x in range(m):
+                    id = "test_id_" + str(next(id_gen))
+                    old = (changed_from.pop())[1]
+                    new = (changed_to.pop())[1]
+
+                    # KGCL change object
+                    change = PredicateChange(
+                        id=id,
+                        about_edge=edge,
+                        old_value=str(old),
+                        new_value=str(new),
+                        object_type=object_type,
+                        language=language,
+                        datatype=datatype,
+                    )
+
+                    kgcl.append(change)
+                    covered.add((s, old, i))
+                    covered.add((s, new, i))
+
+    return kgcl, covered, nonDeterministic
diff --git a/kgcl/diff/diff_2_kgcl_existential.py b/kgcl/diff/diff_2_kgcl_existential.py
new file mode 100644
index 00000000..24024f15
--- /dev/null
+++ b/kgcl/diff/diff_2_kgcl_existential.py
@@ -0,0 +1,189 @@
+import rdflib
+from kgcl.model.kgcl import (
+    NodeRename,
+    NodeObsoletion,
+    NodeUnobsoletion,
+    NodeDeletion,
+    NodeMove,
+    NodeDeepening,
+    NodeShallowing,
+    EdgeCreation,
+    EdgeDeletion,
+    PredicateChange,
+    NodeCreation,
+    ClassCreation,
+    PlaceUnder,
+    RemoveUnder,
+    NewSynonym,
+    RemovedNodeFromSubset,
+)
+from kgcl.diff.graph_diff import (
+    get_added_existentials,
+    get_deleted_existentials,
+)
+from kgcl.diff.render_operations import render
+
+
+def id_generator():
+    id = 0
+    while True:
+        yield id
+        id += 1
+
+
+id_gen = id_generator()
+
+
+class ExistentialChangeSummary:
+    """
+    Dataclass holding information about (atomic) existential restriction changes.
+    """
+
+    def __init__(self):
+
+        self.existential_additions = []
+        self.existential_deletions = []
+        self.covered_triples_triple_existential_additions = []
+        self.covered_triples_triple_existential_deletions = []
+
+    def get_commands(self):
+        kgcl_commands = []
+        for k in self.existential_additions:
+            kgcl_commands.append(k)
+        for k in self.existential_deletions:
+            kgcl_commands.append(k)
+        return kgcl_commands
+
+    def get_existential_additions(self):
+        return self.existential_additions
+
+    def get_existential_deletions(self):
+        return self.existential_deletions
+
+    def get_summary_KGCL_commands(self):
+        out = (
+            "EdgeCreation (existential): " + str(len(self.existential_additions)) + "\n"
+            "EdgeDeletion (existential): " + str(len(self.existential_deletions)) + "\n"
+        )
+        return out
+
+    def get_summary_RDF_triples(self):
+        out = (
+            "EdgeCreation (existential)"
+            + str(len(self.covered_triples_triple_existential_additions))
+            + "\n"
+            "EdgeDeletion (existential)"
+            + str(len(self.covered_triples_triple_existential_deletions))
+            + "\n"
+        )
+        return out
+
+    # RDF data
+
+    def get_covered_triples_existential_additions(self):
+        return self.covered_triples_triple_existential_additions
+
+    def get_covered_triples_existential_deletions(self):
+        return self.covered_triples_triple_existential_deletions
+
+    def add_covered_triples_existential_additions(self, triples):
+        for t in triples:
+            self.covered_triples_triple_existential_additions.append(t)
+
+    def add_covered_triples_existential_deletions(self, triples):
+        for t in triples:
+            self.covered_triples_triple_existential_deletions.append(t)
+
+    # KGCL data
+
+    def add_existential_addition(self, i):
+        self.existential_additions.append(i)
+
+    def add_existential_deletion(self, i):
+        self.existential_deletions.append(i)
+
+
+def generate_atomic_existential_commands(g1, g2):
+    """
+    Given two graphs g1 and g2,
+    return all edge creations and edge deletions.
+    """
+    summary = ExistentialChangeSummary()
+
+    added = get_added_existentials(g1, g2)
+    deleted = get_deleted_existentials(g1, g2)
+
+    existential_additions, covered = generate_existential_additions(added)
+    summary.add_covered_triples_existential_additions(covered)
+
+    existential_deletions, covered = generate_existential_deletions(deleted)
+    summary.add_covered_triples_existential_deletions(covered)
+
+    for a in existential_additions:
+        summary.add_existential_addition(render(a))
+    for d in existential_deletions:
+        summary.add_existential_deletion(render(d))
+
+    return summary
+
+
+def generate_existential_deletions(deleted):
+    """
+    Return EdgeDeletion instances for given (deleted) triples.
+    """
+    covered = rdflib.Graph()
+    kgcl = []
+
+    for a in deleted:
+        subclass = str(a.subclass)
+        property = str(a.property)
+        filler = str(a.filler)
+
+        id = "test_id_" + str(next(id_gen))
+
+        node = EdgeDeletion(
+            id=id,
+            subject=subclass,
+            predicate=property,
+            object=filler,
+            subject_type="uri",
+            predicate_type="uri",
+            object_type="uri",
+        )
+
+        kgcl.append(node)
+        for t in a.triples:
+            covered.add(t)
+
+    return kgcl, covered
+
+
+def generate_existential_additions(added):
+    """
+    Return EdgeCreation instances for given (added) triples.
+    """
+    covered = rdflib.Graph()
+    kgcl = []
+
+    for a in added:
+        subclass = str(a.subclass)
+        property = str(a.property)
+        filler = str(a.filler)
+
+        id = "test_id_" + str(next(id_gen))
+
+        node = EdgeCreation(
+            id=id,
+            subject=subclass,
+            predicate=property,
+            object=filler,
+            subject_type="uri",
+            predicate_type="uri",
+            object_type="uri",
+        )
+
+        kgcl.append(node)
+        for t in a.triples:
+            covered.add(t)
+
+    return kgcl, covered
diff --git a/kgcl/diff/diff_2_kgcl_single.py b/kgcl/diff/diff_2_kgcl_single.py
new file mode 100644
index 00000000..1d0d6cef
--- /dev/null
+++ b/kgcl/diff/diff_2_kgcl_single.py
@@ -0,0 +1,610 @@
+import rdflib
+from rdflib.namespace import (
+    RDFS,
+    RDF,
+    OWL,
+)
+from rdflib import URIRef, Literal
+from kgcl.model.kgcl import (
+    NodeRename,
+    NodeMove,
+    NodeDeepening,
+    NodeShallowing,
+    EdgeCreation,
+    EdgeDeletion,
+    PredicateChange,
+    NodeCreation,
+    ClassCreation,
+    PlaceUnder,
+    RemoveUnder,
+    NewSynonym,
+    RemovedNodeFromSubset,
+)
+
+from kgcl.diff.change_detection import (
+    detect_renamings,
+    detect_node_moves,
+    detect_predicate_changes,
+    detect_annotation_changes,
+)
+
+from kgcl.diff.render_operations import render
+
+from kgcl.diff.graph_diff import (
+    get_added_thin_triples,
+    get_deleted_thin_triples,
+)
+
+
+def id_generator():
+    id = 0
+    while True:
+        yield id
+        id += 1
+
+
+id_gen = id_generator()
+
+
+class SingleTripleChangeSummary:
+    """
+    Dataclass holding information about (atomic) existential restriction changes.
+    """
+
+    def __init__(self):
+
+        # KGCL data
+        self.renamings = []
+        self.class_creations = []
+        self.subsumption_creations = []
+        self.subsumption_deletions = []
+        self.predicate_changes = []
+        self.node_moves = []
+        self.synonym_creations = []
+        self.annotation_changes = []
+
+        # RDF data
+        self.covered_triples_renamings = []
+        self.covered_triples_class_creations = []
+        self.covered_triples_subsumption_creations = []
+        self.covered_triples_subsumption_deletions = []
+        self.covered_triples_predicate_changes = []
+        self.covered_triples_node_moves = []
+        self.covered_triples_synonym_creations = []
+        self.covered_triples_edge_creations = []
+        self.covered_triples_edge_deletions = []
+        self.covered_triples_annotation_changes = []
+
+        # non-deterministic data
+        self.non_deterministic_node_moves = []
+        self.non_deterministic_predicate_changes = []
+        self.non_deterministic_renamings = []
+        self.non_deterministic_annotation_changes = []
+
+    def get_commands(self):
+        kgcl_commands = []
+        for k in self.synonym_creations:
+            kgcl_commands.append(k)
+        for k in self.node_moves:
+            kgcl_commands.append(k)
+        for k in self.predicate_changes:
+            kgcl_commands.append(k)
+        for k in self.renamings:
+            kgcl_commands.append(k)
+        for k in self.class_creations:
+            kgcl_commands.append(k)
+        for k in self.subsumption_creations:
+            kgcl_commands.append(k)
+        for k in self.subsumption_deletions:
+            kgcl_commands.append(k)
+        for k in self.annotation_changes:
+            kgcl_commands.append(k)
+
+        return kgcl_commands
+
+    def get_renamings(self):
+        return self.renamings
+
+    def get_class_creations(self):
+        return self.class_creations
+
+    def get_subsumption_creations(self):
+        return self.subsumption_creations
+
+    def get_subsumption_deletions(self):
+        return self.subsumption_deletions
+
+    def get_predicate_changes(self):
+        return self.predicate_changes
+
+    def get_node_moves(self):
+        return self.node_moves
+
+    def get_synonym_creations(self):
+        return self.synonym_creations
+
+    def get_annotation_changes(self):
+        return self.annotation_changes
+
+    def get_summary_KGCL_commands(self):
+        out = (
+            "Renamings: "
+            + str(len(self.renamings))
+            + "\n"
+            + "ClassCreations: "
+            + str(len(self.class_creations))
+            + "\n"
+            # + "Subsumption Creations: "
+            + "PlaceUnder: "
+            + str(len(self.subsumption_creations))
+            + "\n"
+            # + "Subsumption Deletions: "
+            + "RemoveUnder: "
+            + str(len(self.subsumption_deletions))
+            + "\n"
+            + "PredicateChanges: "
+            + str(len(self.predicate_changes))
+            + "\n"
+            + "NodeMoves: "
+            + str(len(self.node_moves))
+            + "\n"
+            + "SynonymCreations: "
+            + str(len(self.synonym_creations))
+            + "\n"
+            + "NodeAnnotationChanges: "
+            + str(len(self.annotation_changes))
+            + "\n"
+        )
+        return out
+
+    def get_summary_RDF_triples(self):
+        out = (
+            "Renamings:"
+            + str(len(self.covered_triples_renamings))
+            + "\n"
+            + "ClassCreations: "
+            + str(len(self.covered_triples_class_creations))
+            + "\n"
+            # + "Subsumption Creations: "
+            + "PlaceUnder: "
+            + str(len(self.covered_triples_subsumption_creations))
+            + "\n"
+            # + "Subsumption Deletions: "
+            + "RemoveUnder: "
+            + str(len(self.covered_triples_subsumption_deletions))
+            + "\n"
+            + "PredicateChanges: "
+            + str(len(self.covered_triples_predicate_changes))
+            + "\n"
+            + "NodeMoves: "
+            + str(len(self.covered_triples_node_moves))
+            + "\n"
+            + "SynonymCreations: "
+            + str(len(self.covered_triples_synonym_creations))
+            + "\n"
+            + "NodeAnnotationChanges: "
+            + str(len(self.covered_triples_annotation_changes))
+            + "\n"
+        )
+        return out
+
+    # RDF data
+
+    def get_covered_triples_renamings(self):
+        return self.covered_triples_renamings
+
+    def add_covered_triples_renamings(self, triples):
+        for t in triples:
+            self.covered_triples_renamings.append(t)
+
+    def get_covered_triples_class_creations(self):
+        return self.covered_triples_class_creations
+
+    def add_covered_triples_class_creations(self, triples):
+        for t in triples:
+            self.covered_triples_class_creations.append(t)
+
+    def get_covered_triples_subsumption_creations(self):
+        return self.covered_triples_subsumption_creations
+
+    def add_covered_triples_subsumption_creations(self, triples):
+        for t in triples:
+            self.covered_triples_subsumption_creations.append(t)
+
+    def get_covered_triples_subsumption_deletions(self):
+        return self.covered_triples_subsumption_deletions
+
+    def add_covered_triples_subsumption_deletions(self, triples):
+        for t in triples:
+            self.covered_triples_subsumption_deletions.append(t)
+
+    def get_covered_triples_predicate_changes(self):
+        return self.covered_triples_predicate_changes
+
+    def add_covered_triples_predicate_changes(self, triples):
+        for t in triples:
+            self.covered_triples_predicate_changes.append(t)
+
+    def get_covered_triples_node_moves(self):
+        return self.covered_triples_node_moves
+
+    def add_covered_triples_node_moves(self, triples):
+        for t in triples:
+            self.covered_triples_node_moves.append(t)
+
+    def get_covered_triples_synonym_creations(self):
+        return self.covered_triples_synonym_creations
+
+    def add_covered_triples_synonym_creations(self, triples):
+        for t in triples:
+            self.covered_triples_synonym_creations.append(t)
+
+    def get_covered_triples_annotation_changes(self):
+        return self.covered_triples_annotation_changes
+
+    def add_covered_triples_annotation_changes(self, triples):
+        for t in triples:
+            self.covered_triples_annotation_changes.append(t)
+
+    # KGCL data
+
+    def add_edge_creations(self, ls):
+        for i in ls:
+            self.edge_creations.append(i)
+
+    def add_edge_deletions(self, ls):
+        for i in ls:
+            self.edge_deletions.append(i)
+
+    def add_renamings(self, ls):
+        for i in ls:
+            self.renamings.append(i)
+
+    def add_class_creations(self, ls):
+        for i in ls:
+            self.class_creations.append(i)
+
+    def add_subsumption_creations(self, ls):
+        for i in ls:
+            self.subsumption_creations.append(i)
+
+    def add_subsumption_deletions(self, ls):
+        for i in ls:
+            self.subsumption_deletions.append(i)
+
+    def add_edge_creation(self, i):
+        self.edge_creations.append(i)
+
+    def add_edge_deletion(self, i):
+        self.edge_deletions.append(i)
+
+    def add_renaming(self, i):
+        self.renamings.append(i)
+
+    def add_class_creation(self, i):
+        self.class_creations.append(i)
+
+    def add_subsumption_creation(self, i):
+        self.subsumption_creations.append(i)
+
+    def add_subsumption_deletion(self, i):
+        self.subsumption_deletions.append(i)
+
+    def add_predicate_change(self, i):
+        self.predicate_changes.append(i)
+
+    def add_node_move(self, i):
+        self.node_moves.append(i)
+
+    def add_synonym_creation(self, i):
+        self.synonym_creations.append(i)
+
+    def add_annotation_change(self, i):
+        self.annotation_changes.append(i)
+
+    # non-deterministic data
+    def add_non_deterministic_node_moves(self, ls):
+        self.non_deterministic_node_moves += ls
+
+    def add_non_deterministic_predicate_changes(self, ls):
+        self.non_deterministic_predicate_changes += ls
+
+    def add_non_deterministic_renamings(self, ls):
+        self.non_deterministic_renamings += ls
+
+    def add_non_deterministic_annotation_changes(self, ls):
+        self.non_deterministic_annotation_changes += ls
+
+    def get_non_deterministic_node_moves(self):
+        return self.non_deterministic_node_moves
+
+    def get_non_deterministic_predicate_changes(self):
+        return self.non_deterministic_predicate_changes
+
+    def get_non_deterministic_renamings(self):
+        return self.non_deterministic_renamings
+
+    def get_non_deterministic_annotation_changes(self):
+        return self.non_deterministic_annotation_changes
+
+
+def generate_thin_triple_commands(g1, g2):
+    """
+    Given two graphs g1 and g2,
+    return all KGCL data model instances to account for their diff.
+    """
+    # summary object for single triple changes
+    summary = SingleTripleChangeSummary()
+
+    added = get_added_thin_triples(g1, g2)
+    deleted = get_deleted_thin_triples(g1, g2)
+
+    # synonyms
+    synonym_additions, covered = generate_synonym_creations(added)
+    summary.add_covered_triples_synonym_creations(covered)
+    added = added - covered
+    # TODO: extend data model for deleted synonyms
+    # synonym_deletions, covered = generate_synonym_deletions(added)
+
+    # deepen [TODO: need some kind of reasoning/querying for this]
+    # shallow [TODO: need some kind of reasoning/querying for this]
+
+    # move
+    node_moves, covered, non_deterministic = detect_node_moves(added, deleted)
+    summary.add_covered_triples_node_moves(covered)
+    summary.add_non_deterministic_node_moves(non_deterministic)
+    added = added - covered
+    deleted = deleted - covered
+
+    # change relationship
+    relationship_change, covered, non_deterministic = detect_predicate_changes(
+        added, deleted
+    )
+    summary.add_covered_triples_predicate_changes(covered)
+    summary.add_non_deterministic_predicate_changes(non_deterministic)
+    added = added - covered
+    deleted = deleted - covered
+
+    # renamings
+    renamings, covered, non_deterministic = detect_renamings(added, deleted)
+    summary.add_covered_triples_renamings(covered)
+    summary.add_non_deterministic_renamings(non_deterministic)
+    added = added - covered
+    deleted = deleted - covered
+
+    # subsumptions
+    subsumption_creations, covered = generate_subsumption_creations(added)
+    summary.add_covered_triples_subsumption_creations(covered)
+    added = added - covered
+
+    subsumption_deletions, covered = generate_subsumption_deletions(deleted)
+    summary.add_covered_triples_subsumption_deletions(covered)
+    deleted = deleted - covered
+
+    # create node
+    node_creations, covered = generate_class_creations(added)
+    summary.add_covered_triples_class_creations(covered)
+    added = added - covered
+
+    # create annotation changes
+    old_annotation_properties = get_annotation_properties(g1)
+    new_annotation_properties = get_annotation_properties(g2)
+    annotation_changes, covered, non_deterministic = detect_annotation_changes(
+        added, deleted, new_annotation_properties, old_annotation_properties
+    )
+    summary.add_covered_triples_annotation_changes(covered)
+    summary.add_non_deterministic_annotation_changes(non_deterministic)
+    added = added - covered
+    deleted = deleted - covered
+
+    for s in synonym_additions:
+        summary.add_synonym_creation(render(s))
+    for c in node_creations:
+        summary.add_class_creation(render(c))
+    for m in node_moves:
+        summary.add_node_move(render(m))
+    for r in relationship_change:
+        summary.add_predicate_change(render(r))
+    for s in subsumption_creations:
+        summary.add_subsumption_creation(render(s))
+    for s in subsumption_deletions:
+        summary.add_subsumption_deletion(render(s))
+    for r in renamings:
+        summary.add_renaming(render(r))
+    for c in annotation_changes:
+        summary.add_annotation_change(render(c))
+
+    return summary
+
+
+def get_annotation_properties(graph):
+    properties = set()
+    for s, p, o in graph.triples((None, RDF.type, OWL.AnnotationProperty)):
+        properties.add(s)
+    return properties
+
+
+def get_type(rdf_entity):
+    if isinstance(rdf_entity, URIRef):
+        return "uri"
+    elif isinstance(rdf_entity, Literal):
+        return "literal"
+    else:
+        return "Error"
+
+
+def get_language_tag(rdf_entity):
+    if isinstance(rdf_entity, Literal):
+        return rdf_entity.language
+    else:
+        return None
+
+
+def get_datatype(rdf_entity):
+    if isinstance(rdf_entity, Literal) and rdf_entity.datatype is not None:
+        return str(rdf_entity.datatype)
+    else:
+        return None
+
+
+# TODO: identify node creations + labels
+def generate_class_creations(added):
+    covered = rdflib.Graph()
+    kgcl = []
+
+    for s, p, o in added.triples((None, RDF.type, OWL.Class)):
+        id = "test_id_" + str(next(id_gen))
+        covered.add((s, p, o))
+        node = ClassCreation(id=id, about_node=str(s), node_id=str(s))
+        kgcl.append(node)
+
+    return kgcl, covered
+
+
+def generate_synonym_creations(added):
+    covered = rdflib.Graph()
+    kgcl = []
+
+    synonym = URIRef("http://www.geneontology.org/formats/oboInOwl#hasSynonym")
+    exact_synonym = URIRef(
+        "http://www.geneontology.org/formats/oboInOwl#hasExactSynonym"
+    )
+    narrow_synonym = URIRef(
+        "http://www.geneontology.org/formats/oboInOwl#hasNarrowSynonym"
+    )
+    broad_synonym = URIRef(
+        "http://www.geneontology.org/formats/oboInOwl#hasBroadSynonym"
+    )
+    related_synonym = URIRef(
+        "http://www.geneontology.org/formats/oboInOwl#hasRelatedSynonym"
+    )
+
+    include = {synonym, exact_synonym, narrow_synonym, broad_synonym, related_synonym}
+
+    # synonyms
+    for s, p, o in added:
+        if p in include:
+            id = "test_id_" + str(next(id_gen))
+            covered.add((s, p, o))
+
+            language = get_language_tag(o)
+            # datatype = get_datatype(o)
+
+            qualifier = ""  # case for hasSynonym
+            if p == exact_synonym:
+                qualifier = "exact"
+            elif p == narrow_synonym:
+                qualifier = "narrow"
+            elif p == broad_synonym:
+                qualifier = "broad"
+            elif p == related_synonym:
+                qualifier = "related"
+
+            node = NewSynonym(
+                id=id,
+                about_node=str(s),
+                new_value=str(o),
+                qualifier=qualifier,
+                language=language,
+            )
+
+            kgcl.append(node)
+
+    return kgcl, covered
+
+
+# TODO: extend data model for deleted synonyms
+def generate_synonym_deletions(deleted):
+    covered = rdflib.Graph()
+    kgcl = []
+
+    # synonym = URIRef("http://www.geneontology.org/formats/oboInOwl#hasSynonym")
+    exact_synonym = URIRef(
+        "http://www.geneontology.org/formats/oboInOwl#hasExactSynonym"
+    )
+    narrow_synonym = URIRef(
+        "http://www.geneontology.org/formats/oboInOwl#hasNarrowSynonym"
+    )
+    broad_synonym = URIRef(
+        "http://www.geneontology.org/formats/oboInOwl#hasBroadSynonym"
+    )
+    related_synonym = URIRef(
+        "http://www.geneontology.org/formats/oboInOwl#hasRelatedSynonym"
+    )
+
+    # synonyms
+    for s, p, o in deleted:
+        id = "test_id_" + str(next(id_gen))
+        covered.add((s, p, o))
+
+        language = get_language_tag(o)
+        # datatype = get_datatype(o)
+
+        qualifier = ""  # case for hasSynonym
+        if p == exact_synonym:
+            qualifier = "exact"
+        elif p == narrow_synonym:
+            qualifier = "narrow"
+        elif p == broad_synonym:
+            qualifier = "broad"
+        elif p == related_synonym:
+            qualifier = "related"
+
+        # TODO: extend data model for deleted synonyms
+        node = NewSynonym(
+            id=id,
+            about_node=str(s),
+            new_value=str(o),
+            qualifier=qualifier,
+            language=language,
+        )
+
+        kgcl.append(node)
+
+    return kgcl, covered
+
+
+def generate_subsumption_creations(added):
+    covered = rdflib.Graph()
+    kgcl = []
+
+    for s, p, o in added.triples((None, RDFS.subClassOf, None)):
+        id = "test_id_" + str(next(id_gen))
+        subclass = str(s)
+        superclass = str(o)
+
+        # TODO the hardcoded owl:subClassOf should be part of the data model
+        node = PlaceUnder(
+            id=id,
+            subject=subclass,
+            predicate="<http://www.w3.org/2000/01/rdf-schema#subClassOf>",
+            object=superclass,
+        )
+
+        kgcl.append(node)
+        covered.add((s, p, o))
+
+    return kgcl, covered
+
+
+def generate_subsumption_deletions(deleted):
+    covered = rdflib.Graph()
+    kgcl = []
+
+    for s, p, o in deleted.triples((None, RDFS.subClassOf, None)):
+        id = "test_id_" + str(next(id_gen))
+        subclass = str(s)
+        superclass = str(o)
+
+        # TODO the hardcoded owl:subClassOf should be part of the data model
+        node = RemoveUnder(
+            id=id,
+            subject=subclass,
+            predicate="<http://www.w3.org/2000/01/rdf-schema#subClassOf>",
+            object=superclass,
+        )
+
+        kgcl.append(node)
+        covered.add((s, p, o))
+
+    return kgcl, covered
diff --git a/kgcl/diff/diff_2_kgcl_triple_annotation.py b/kgcl/diff/diff_2_kgcl_triple_annotation.py
new file mode 100644
index 00000000..c7c275b1
--- /dev/null
+++ b/kgcl/diff/diff_2_kgcl_triple_annotation.py
@@ -0,0 +1,254 @@
+import rdflib
+from rdflib import URIRef, Literal
+from kgcl.model.kgcl import (
+    NodeRename,
+    NodeObsoletion,
+    NodeUnobsoletion,
+    NodeDeletion,
+    NodeMove,
+    NodeDeepening,
+    NodeShallowing,
+    EdgeCreation,
+    EdgeDeletion,
+    PredicateChange,
+    NodeCreation,
+    ClassCreation,
+    PlaceUnder,
+    RemoveUnder,
+    NewSynonym,
+    RemovedNodeFromSubset,
+)
+from kgcl.diff.graph_diff import (
+    get_added_triple_annotations,
+    get_deleted_triple_annotations,
+)
+from kgcl.diff.render_operations import render
+from kgcl.model.ontology_model import Annotation
+
+
+def id_generator():
+    id = 0
+    while True:
+        yield id
+        id += 1
+
+
+id_gen = id_generator()
+
+
+class TripleAnnotationChangeSummary:
+    """
+    Dataclass holding information about triple annotation axioms.
+    """
+
+    def __init__(self):
+
+        self.triple_annotations_additions = []
+        self.triple_annotations_deletions = []
+        self.covered_triples_triple_annotations_additions = []
+        self.covered_triples_triple_annotations_deletions = []
+
+    def get_commands(self):
+        kgcl_commands = []
+        for k in self.triple_annotations_additions:
+            kgcl_commands.append(k)
+        for k in self.triple_annotations_deletions:
+            kgcl_commands.append(k)
+        return kgcl_commands
+
+    def get_triple_annotation_additions(self):
+        return self.triple_annotations_additions
+
+    def get_triple_annotation_deletions(self):
+        return self.triple_annotations_deletions
+
+    def get_summary_KGCL_commands(self):
+        out = (
+            # "Triple Annotation Additions: "
+            "EdgeCreations (with annotation): "
+            + str(len(self.triple_annotations_additions))
+            + "\n"
+            # "Triple Annotation Deletions: "
+            + "EdgeDeletion (with annotation): "
+            + str(len(self.triple_annotations_deletions))
+            + "\n"
+        )
+        return out
+
+    def get_summary_RDF_triples(self):
+        out = (
+            # "Triple Annotation Additions: "
+            "EdgeCreations (with annotation): "
+            + str(len(self.covered_triples_triple_annotations_additions))
+            + "\n"
+            # + "Triple Annotation Deletions: "
+            + "EdgeDeletion (with annotation): "
+            + str(len(self.covered_triples_triple_annotations_deletions))
+            + "\n"
+        )
+        return out
+
+    # RDF data
+
+    def get_covered_triples_annotation_additions(self):
+        return self.covered_triples_triple_annotations_additions
+
+    def get_covered_triples_annotation_deletions(self):
+        return self.covered_triples_triple_annotations_deletions
+
+    def add_covered_triples_annotation_additions(self, triples):
+        for t in triples:
+            self.covered_triples_triple_annotations_additions.append(t)
+
+    def add_covered_triples_annotation_deletions(self, triples):
+        for t in triples:
+            self.covered_triples_triple_annotations_deletions.append(t)
+
+    # KGCL data
+
+    def add_triple_annotation_addition(self, i):
+        self.triple_annotations_additions.append(i)
+
+    def add_triple_annotation_deletion(self, i):
+        self.triple_annotations_deletions.append(i)
+
+
+def generate_triple_annotation_commands(g1, g2):
+    """
+    Given two graphs g1 and g2,
+    return all (annotated) EdgeCreation and
+    (annotated) EdgeDeletions to account for their diff.
+    """
+    summary = TripleAnnotationChangeSummary()
+
+    added = get_added_triple_annotations(g1, g2)
+    deleted = get_deleted_triple_annotations(g1, g2)
+
+    additions, covered = generate_triple_annotation_additions(added)
+    summary.add_covered_triples_annotation_additions(covered)
+
+    deletions, covered = generate_triple_annotation_deletions(deleted)
+    summary.add_covered_triples_annotation_deletions(covered)
+
+    for a in additions:
+        summary.add_triple_annotation_addition(render(a))
+    for d in deletions:
+        summary.add_triple_annotation_deletion(render(d))
+
+    return summary
+
+
+def generate_triple_annotation_additions(added):
+    """
+    Return EdgeCreation instances for given (added) triples.
+    """
+    covered = rdflib.Graph()
+    kgcl = []
+
+    for a in added:
+        source = str(a.source)
+        property = str(a.property)
+        target = str(a.target)
+        target_type = get_type(a.target)
+        annotation_property = str(a.annotation_property)
+        annotation = str(a.annotation)
+        annotation_type = get_type(a.annotation)
+
+        id = "test_id_" + str(next(id_gen))
+
+        language = get_language_tag(a.target)
+        datatype = get_datatype(a.target)
+        if datatype is not None:
+            datatype = "<" + datatype + ">"  # expect data types without curies
+
+        annotation = Annotation(
+            property=annotation_property, filler=annotation, filler_type=annotation_type
+        )
+
+        node = EdgeCreation(
+            id=id,
+            subject=source,
+            predicate=property,
+            object=target,
+            object_type=target_type,
+            annotation_set=annotation,
+            language=language,
+            datatype=datatype,
+        )
+
+        kgcl.append(node)
+        for t in a.triples:
+            covered.add(t)
+
+    return kgcl, covered
+
+
+def generate_triple_annotation_deletions(deleted):
+    """
+    Return EdgeDeletion instances for given (deleted) triples.
+    """
+    covered = rdflib.Graph()
+    kgcl = []
+
+    for a in deleted:
+        source = str(a.source)
+        property = str(a.property)
+        target = str(a.target)
+        target_type = get_type(a.target)
+        annotation_property = str(a.annotation_property)
+        annotation = str(a.annotation)
+        annotation_type = get_type(a.annotation)
+
+        id = "test_id_" + str(next(id_gen))
+
+        language = get_language_tag(a.target)
+        datatype = get_datatype(a.target)
+        if datatype is not None:
+            datatype = "<" + datatype + ">"  # expect data types without curies
+
+        annotation = Annotation(
+            property=annotation_property, filler=annotation, filler_type=annotation_type
+        )
+
+        node = EdgeDeletion(
+            id=id,
+            subject=source,
+            predicate=property,
+            object=target,
+            object_type=target_type,
+            annotation_set=annotation,
+            language=language,
+            datatype=datatype,
+        )
+
+        kgcl.append(node)
+        for t in a.triples:
+            covered.add(t)
+
+    return kgcl, covered
+
+
+# TODO: factor these out in some kind of utility library
+
+
+def get_type(rdf_entity):
+    if isinstance(rdf_entity, URIRef):
+        return "uri"
+    elif isinstance(rdf_entity, Literal):
+        return "literal"
+    else:
+        return "Error"
+
+
+def get_language_tag(rdf_entity):
+    if isinstance(rdf_entity, Literal):
+        return rdf_entity.language
+    else:
+        return None
+
+
+def get_datatype(rdf_entity):
+    if isinstance(rdf_entity, Literal) and rdf_entity.datatype is not None:
+        return str(rdf_entity.datatype)
+    else:
+        return None
diff --git a/kgcl/diff/graph_diff.py b/kgcl/diff/graph_diff.py
new file mode 100644
index 00000000..f7495650
--- /dev/null
+++ b/kgcl/diff/graph_diff.py
@@ -0,0 +1,115 @@
+import rdflib
+from .owlstar_sublanguage import (
+    get_thin_triples,
+    get_atomic_existentials,
+    get_triple_annotations,
+    get_atomic_subsumptions,
+)
+
+
+def get_added_thin_triples(g1, g2):
+    """
+    Given two graphs g1 and g2,
+    returns triples (without blank nodes) that are in g2 but not g1,
+    i.e. 'added' triples.
+    """
+    thin1 = get_thin_triples(g1)
+    thin2 = get_thin_triples(g2)
+
+    return thin2 - thin1
+
+
+def get_deleted_thin_triples(g1, g2):
+    """
+    Given two graphs g1 and g2,
+    returns triples (without blank nodes) that are in g1 but not g2,
+    i.e. 'deleted' triples.
+    """
+    thin1 = get_thin_triples(g1)
+    thin2 = get_thin_triples(g2)
+
+    return thin1 - thin2
+
+
+def get_added_subsumptions(g1, g2):
+    """
+    Given two graphs g1 and g2,
+    returns atomic subsumption axioms that are in g2 but not g1,
+    i.e. 'added' subsumptions.
+    """
+    sub1 = get_atomic_subsumptions(g1)
+    sub2 = get_atomic_subsumptions(g2)
+
+    return sub2 - sub1
+
+
+def get_deleted_subsumptions(g1, g2):
+    """
+    Given two graphs g1 and g2,
+    returns atomic subsumption axioms that are in g1 but not g2,
+    i.e. 'deleted' subsumptions.
+    """
+    sub1 = get_atomic_subsumptions(g1)
+    sub2 = get_atomic_subsumptions(g2)
+
+    return sub1 - sub2
+
+
+# NB: this returns a set of 'ExistentialRestrictions' (defined in owlstar_sublanguage)
+def get_added_existentials(g1, g2):
+    """
+    Given two graphs g1 and g2,
+    returns ExistentialRestrictions that are in g2 but not g1,
+    i.e. 'added' ExistentialRestrictions.
+    """
+    ex1 = set(get_atomic_existentials(g1))
+    ex2 = set(get_atomic_existentials(g2))
+
+    return ex2 - ex1
+
+
+def get_deleted_existentials(g1, g2):
+    """
+    Given two graphs g1 and g2,
+    returns ExistentialRestrictions that are in g1 but not g2,
+    i.e. 'deleted' ExistentialRestrictions.
+    """
+    ex1 = set(get_atomic_existentials(g1))
+    ex2 = set(get_atomic_existentials(g2))
+
+    return ex1 - ex2
+
+
+# NB this returns a set of 'TripleAnnotation's (defined in owlstar_sublanguage)
+def get_added_triple_annotations(g1, g2):
+    """
+    Given two graphs g1 and g2,
+    returns ExistentialRestrictions that are in g2 but not g1,
+    i.e. 'added' ExistentialRestrictions.
+    """
+    annotations1 = set(get_triple_annotations(g1))
+    annotations2 = set(get_triple_annotations(g2))
+
+    return annotations2 - annotations1
+
+
+def get_deleted_triple_annotations(g1, g2):
+    """
+    Given two graphs g1 and g2,
+    returns TripleAnnotations that are in g1 but not g2,
+    i.e. 'deleted' ExistentialRestrictions.
+    """
+    annotations1 = set(get_triple_annotations(g1))
+    annotations2 = set(get_triple_annotations(g2))
+
+    return annotations1 - annotations2
+
+
+if __name__ == "__main__":
+    g1 = rdflib.Graph()
+    g2 = rdflib.Graph()
+
+    g1.load("n3/obi/obi_1.nt", format="nt")
+    g2.load("n3/obi/obi_2.nt", format="nt")
+
+    # deleted = get_deleted_triples(g1, g2)
diff --git a/kgcl/diff/owlstar_sublanguage.py b/kgcl/diff/owlstar_sublanguage.py
new file mode 100644
index 00000000..8c988f0d
--- /dev/null
+++ b/kgcl/diff/owlstar_sublanguage.py
@@ -0,0 +1,375 @@
+import rdflib
+from rdflib.namespace import (
+    RDFS,
+    OWL,
+    RDF,
+)
+from rdflib import BNode
+import time
+
+
+class TripleAnnotation:
+    # TODO: store blank node associated with the annotation
+    def __init__(self, s, p, t, ap, a):
+        self.source = s
+        self.property = p
+        self.target = t
+        self.annotation_property = ap
+        self.annotation = a
+        self.triples = set()
+
+    # TODO check that all triples have the same blank node
+    def add_triple(self, t):
+        self.triples.add(t)
+
+    def add_triples(self, ts):
+        self.triples.update(ts)
+
+    def __repr__(self):
+        return (
+            "<<"
+            + str(self.source)
+            + " "
+            + str(self.property)
+            + " "
+            + str(self.target)
+            + ">> "
+            + str(self.annotation_property)
+            + " "
+            + str(self.annotation)
+        )
+
+    def __eq__(self, other):
+        if isinstance(other, TripleAnnotation):
+            return (
+                self.source == other.source
+                and self.property == other.property
+                and self.target == other.target
+                and self.annotation_property == other.annotation_property
+                and self.annotation == other.annotation
+            )
+        return False
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __key(self):
+        return (
+            self.source,
+            self.property,
+            self.target,
+            self.annotation_property,
+            self.annotation,
+        )
+
+    def __hash__(self):
+        return hash(self.__key())
+
+
+class ExistentialRestriction:
+    # TODO: store blank node associated with the existential restriction
+    def __init__(self, sub, prop, fil):
+        self.subclass = sub
+        self.property = prop
+        self.filler = fil
+        self.triples = set()
+
+    def add_triple(self, t):
+        self.triples.add(t)
+
+    def add_triples(self, ts):
+        self.triples.update(ts)
+
+    def __repr__(self):
+        return (
+            self.subclass
+            + " "
+            + "SubClassOf"
+            + " "
+            + self.property
+            + " "
+            + "some"
+            + " "
+            + self.filler
+        )
+
+    def __eq__(self, other):
+        if isinstance(other, ExistentialRestriction):
+            return (
+                self.subclass == other.subclass
+                and self.property == other.property
+                and self.filler == other.filler
+            )
+        return False
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __key(self):
+        return (
+            self.subclass,
+            self.property,
+            self.filler,
+        )
+
+    def __hash__(self):
+        return hash(self.__key())
+
+
+def get_thin_triples(g):
+    res = rdflib.Graph()
+    for s, p, o in g.triples((None, None, None)):
+        if (
+            not isinstance(s, BNode)
+            and not isinstance(o, BNode)
+            # and p != RDFS.subClassOf
+        ):
+            res.add((s, p, o))
+
+    return res
+
+
+def get_atomic_subsumptions(g):
+    res = rdflib.Graph()
+    for s, p, o in g.triples((None, RDFS.subClassOf, None)):
+        if not isinstance(s, BNode) and not isinstance(o, BNode):
+            res.add((s, p, o))
+
+    return res
+
+
+def get_atomic_existentials(g):
+    some_values_from = set(g.subjects(predicate=OWL.someValuesFrom))
+    on_property = set(g.subjects(predicate=OWL.onProperty))
+
+    intersection = some_values_from & on_property
+
+    existential_2_classes = {}  # map from blank node to (named) subclasses
+    for i in intersection:
+        existential_2_classes[i] = []
+        if isinstance(i, BNode):  # this check should be unnecessary
+            for s, p, o in g.triples((None, RDFS.subClassOf, i)):
+                if not isinstance(s, BNode):
+                    existential_2_classes[i].append(str(s))
+
+    owlstar_axiom = []
+    bnodes_2_existentials = get_bnodes_2_atomic_existentials(g)
+    for i, subclasses in existential_2_classes.items():
+        # bnode = str(i)  # don't really need the bnode
+        filler = ""
+        bnode_filler = True
+        property = ""
+        bnode_property = True
+
+        # get property
+        for s, p, o in g.triples((i, OWL.onProperty, None)):
+            if not isinstance(o, BNode):  # this test is unnecessary
+                property = str(o)
+                bnode_property = False
+
+        # get filler
+        for s, p, o in g.triples((i, OWL.someValuesFrom, None)):
+            if not isinstance(o, BNode):
+                filler = str(o)
+                bnode_filler = False
+
+        if not bnode_property and not bnode_filler:
+            for subclass in subclasses:
+                ex = ExistentialRestriction(subclass, property, filler)
+                ex.add_triples(bnodes_2_existentials[i])
+                owlstar_axiom.append(ex)
+                # owlstar_axiom.append((subclass, property, filler))
+
+    return owlstar_axiom
+
+
+def get_bnodes_2_atomic_existentials(g):
+    some_values_from = set(g.subjects(predicate=OWL.someValuesFrom))
+    on_property = set(g.subjects(predicate=OWL.onProperty))
+    # restriction = set(g.subjects(predicate=rdf.type)) # Restriction
+
+    intersection = some_values_from & on_property
+
+    existential_2_classes = {}  # map from blank node to (named) subclass
+    for i in intersection:
+        existential_2_classes[i] = []
+        if isinstance(i, BNode):  # this check should be unnecessary
+            for s, p, o in g.triples((None, RDFS.subClassOf, i)):
+                if not isinstance(s, BNode):
+                    existential_2_classes[i].append(s)
+
+    bnode_triples = {}
+    for i, subclasses in existential_2_classes.items():
+        bnode_triples[i] = []
+
+        filler = i
+        bnode_filler = True
+        property = i
+        bnode_property = True
+
+        # get property
+        for s, p, o in g.triples((i, OWL.onProperty, None)):
+            if not isinstance(o, BNode):
+                property = o
+                bnode_property = False
+
+        # get filler
+        for s, p, o in g.triples((i, OWL.someValuesFrom, None)):
+            if not isinstance(o, BNode):
+                filler = o
+                bnode_filler = False
+
+        if not bnode_property and not bnode_filler:
+            for subclass in subclasses:
+                bnode_triples[i].append((i, OWL.some_values_from, filler))
+                bnode_triples[i].append((i, OWL.onProperty, property))
+                bnode_triples[i].append((subclass, RDFS.subClassOf, i))
+
+    return bnode_triples
+
+
+def get_triple_annotations(g):
+    source = set(g.subjects(predicate=OWL.annotatedSource))
+    property = set(g.subjects(predicate=OWL.annotatedProperty))
+    target = set(g.subjects(predicate=OWL.annotatedTarget))
+
+    intersection = source & property & target
+
+    exclude = {
+        OWL.annotatedSource,
+        OWL.annotatedProperty,
+        OWL.annotatedTarget,
+        RDF.type,
+    }
+
+    bnodes_2_triple_annotations = get_bnodes_2_triple_annotations(g)
+    annotations = []
+    for i in intersection:
+        if isinstance(i, BNode):  # this check should be unnecessary
+            # NB these generators are singletons
+            source = next(g.objects(subject=i, predicate=OWL.annotatedSource))
+            property = next(g.objects(subject=i, predicate=OWL.annotatedProperty))
+            target = next(g.objects(subject=i, predicate=OWL.annotatedTarget))
+            for s, p, o in g.triples((i, None, None)):
+                if (
+                    p not in exclude
+                    and not isinstance(o, BNode)
+                    and not isinstance(p, BNode)
+                    and not isinstance(source, BNode)
+                    and not isinstance(property, BNode)
+                    and not isinstance(target, BNode)
+                ):
+                    ta = TripleAnnotation(source, property, target, p, o)
+                    ta.add_triples(bnodes_2_triple_annotations[s])
+                    annotations.append(ta)
+
+                    # annotations.append((source, property, target, p, o))
+                    # (str(source), str(property), str(target), str(p), str(o))
+
+    return annotations
+
+
+def get_bnodes_2_triple_annotations(g):
+    source = set(g.subjects(predicate=OWL.annotatedSource))
+    property = set(g.subjects(predicate=OWL.annotatedProperty))
+    target = set(g.subjects(predicate=OWL.annotatedTarget))
+
+    # get blank nodes that have all three required predicates
+    intersection = source & property & target
+
+    exclude = {
+        OWL.annotatedSource,
+        OWL.annotatedProperty,
+        OWL.annotatedTarget,
+        # RDF.type,
+    }
+
+    annotations = {}
+    for i in intersection:
+        annotations[i] = []
+        if isinstance(i, BNode):  # this check should be unnecessary
+            # NB these generators are singletons
+            source = next(g.objects(subject=i, predicate=OWL.annotatedSource))
+            property = next(g.objects(subject=i, predicate=OWL.annotatedProperty))
+            target = next(g.objects(subject=i, predicate=OWL.annotatedTarget))
+
+            annotations[i].append((i, OWL.annotatedSource, source))
+            annotations[i].append((i, OWL.annotatedProperty, property))
+            annotations[i].append((i, OWL.annotatedTarget, target))
+
+            for s, p, o in g.triples((i, None, None)):
+                if (
+                    p not in exclude
+                    and not isinstance(o, BNode)
+                    and not isinstance(p, BNode)
+                    and not isinstance(source, BNode)
+                    and not isinstance(property, BNode)
+                    and not isinstance(target, BNode)
+                ):
+                    annotations[i].append((s, p, o))
+
+    return annotations
+
+
+def render_triple_annotation(a):
+    return (
+        "<<"
+        # + a[0].n3()
+        + a[0]
+        + " "
+        # + a[1].n3()
+        + a[1]
+        + " "
+        # + a[2].n3()
+        + a[2]
+        + ">> "
+        # + a[3].n3()
+        + a[3]
+        + " "
+        # + a[4].n3()
+        + a[4]
+    )
+
+
+def render_atomic_existential(a):
+    return (
+        "<<"
+        + a[0]
+        + " "
+        + a[1]
+        + " "
+        + a[2]
+        + ">> "
+        + "os:interpretation"
+        + " "
+        + "os:AllSomeInterpretation"
+    )
+
+
+if __name__ == "__main__":
+    g = rdflib.Graph()
+
+    start = time.time()
+    g.load("n3/obi/obi_1.nt", format="nt")
+    # g.load("n3/uberon/uberon_1.nt", format="nt")
+    end = time.time()
+    print(end - start)
+
+    start = time.time()
+    thin = get_thin_triples(g)
+    end = time.time()
+    print(end - start)
+
+    start = time.time()
+    axioms = get_atomic_existentials(g)
+    end = time.time()
+    print(end - start)
+
+    print(len(axioms))
+
+    annotations = get_triple_annotations(g)
+    print(len(annotations))
+    # for a in annotations:
+    #    print(str(a[0]) + " " + str(a[1]))
+    # for a in axioms:
+    # print(a)
diff --git a/kgcl/diff/pretty_print_kgcl.py b/kgcl/diff/pretty_print_kgcl.py
new file mode 100644
index 00000000..bcaac55f
--- /dev/null
+++ b/kgcl/diff/pretty_print_kgcl.py
@@ -0,0 +1,365 @@
+from rdflib.namespace import RDFS
+from kgcl.model.kgcl import (
+    NodeRename,
+    NodeObsoletion,
+    NodeUnobsoletion,
+    NodeDeletion,
+    NodeMove,
+    NodeDeepening,
+    NodeShallowing,
+    NodeAnnotationChange,
+    EdgeCreation,
+    EdgeDeletion,
+    PredicateChange,
+    NodeCreation,
+    ClassCreation,
+    NewSynonym,
+    PlaceUnder,
+    RemoveUnder,
+    RemovedNodeFromSubset,
+)
+import kgcl.grammar.parser
+
+
+# TODO: maintain this dictionary in a file
+prefix_2_uri = {
+    "obo": "http://purl.obolibrary.org/obo/",
+    "ex": "http://example.org/",
+    "oboInOwl": "http://www.geneontology.org/formats/oboInOwl#",
+    "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
+    "xsd": "http://www.w3.org/2001/XMLSchema#",
+    "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
+    "owl": "http://www.w3.org/2002/07/owl#",
+    "swrl": "http://www.w3.org/2003/11/swrl#",
+    "oio": "http://www.geneontology.org/formats/oboInOwl#",
+    "dce": "http://purl.org/dc/elements/1.1/",
+    "dct": "http://purl.org/dc/terms/",
+    "foaf": "http://xmlns.com/foaf/0.1/",
+    "protege": "http://protege.stanford.edu/plugins/owl/protege#",
+    "BFO": "http://purl.obolibrary.org/obo/BFO_",
+    "CHEBI": "http://purl.obolibrary.org/obo/CHEBI_",
+    "CL": "http://purl.obolibrary.org/obo/CL_",
+    "IAO": "http://purl.obolibrary.org/obo/IAO_",
+    "NCBITaxon": "http://purl.obolibrary.org/obo/NCBITaxon_",
+    "OBI": "http://purl.obolibrary.org/obo/OBI_",
+    "PR": "http://purl.obolibrary.org/obo/PR_",
+    "obo": "http://purl.obolibrary.org/obo/",
+    "UP": "http://purl.uniprot.org/uniprot/",
+    "UC": "http://purl.uniprot.org/core/",
+    "PRO": "http://www.uniprot.org/annotation/PRO_",
+    "faldo": "http://biohackathon.org/resource/faldo#",
+}
+
+
+def get_labels(graph):
+    """
+    Returns a map from IRIs in a graph to (a set of) labels.
+    """
+    entity_2_label = {}
+    for s, p, o in graph.triples((None, RDFS.label, None)):
+        ss = str(s)
+        os = str(o)
+        if "'" not in os:
+            if ss not in entity_2_label:
+                entity_2_label[ss] = []
+            entity_2_label[ss].append(os)
+
+    return entity_2_label
+
+
+def render_instances(kgcl_patch, graph):
+    """
+    Takes a KGCL patch for a graph and
+    returns a more readable patch
+    in which IRI's are replaced by CURIEs and labels where possible.
+    """
+    labelling = get_labels(graph)
+
+    pretty_print_kgcl_patch = []
+    for k in kgcl_patch:
+        kgcl_instance = kgcl.grammar.parser.parse_statement(k)
+        # render_instance(kgcl_instance, labelling)
+        pretty_print_kgcl_patch.append(render_instance(kgcl_instance, labelling))
+
+    return pretty_print_kgcl_patch
+
+
+def has_label(entity, labelling):
+    """
+    Returns the label for an entity if it exists
+    and the entity itself otherwise.
+    """
+    if entity in labelling:
+        return "'" + labelling[entity][0] + "'"
+    else:
+        return entity
+
+
+def curie_entity(entity):
+    """
+    Returns the CURIE for an entity if it exists
+    and the entity itself otherwise.
+    """
+    for prefix, curie in prefix_2_uri.items():
+        if curie in entity:
+            return entity.replace(curie, prefix + ":")[1:-1]
+    return entity
+
+
+def render_entity(entity, type, labelling):
+    """
+    Returns an encoding of the given entity using either
+    a CURIE, a label, a literal, or a URI.
+    """
+    entity = str(entity)
+    entity = repr(entity)[1:-1]
+    if type == "uri":
+        labelling = has_label(entity[1:-1], labelling)
+        if entity[1:-1] != labelling:
+            return labelling
+
+        curing = curie_entity(entity)
+        if entity != curing:
+            return curing
+        # return "<" + entity + ">"
+        return entity
+
+    elif type == "label":
+        if "'" not in entity:
+            return "'" + entity + "'"
+
+    elif type == "literal":
+        if '"' not in entity:
+            return '"' + entity + '"'
+        elif "'''" not in entity and entity[-1] != "'":
+            return "'''" + entity + "'''"
+        elif '"""' not in entity and entity[-1] != '"':
+            return '"""' + entity + '"""'
+        else:
+            # return "Error  " + entity
+            print("Rendering error: " + entity)
+            raise
+    else:
+        # return "Error  " + entity
+        print("Rendering error: " + entity)
+        raise
+
+
+def render_instance(kgcl_instance, labelling):
+    if type(kgcl_instance) is NodeRename:
+        # TODO: subject could be 'None'? (not in KGCL Diff)
+        # passing an empty labelling because we do not want to repeat labels
+        subject = render_entity(kgcl_instance.about_node, "uri", {})
+        old = kgcl_instance.old_value
+        new = kgcl_instance.new_value
+
+        new_language = kgcl_instance.new_language
+        old_language = kgcl_instance.old_language
+
+        if old_language is not None:
+            old = old + "@" + old_language
+
+        if new_language is not None:
+            new = new + "@" + new_language
+
+        return "rename " + subject + " from " + old + " to " + new
+
+    if type(kgcl_instance) is NodeObsoletion:
+        subject = render_entity(kgcl_instance.about_node, "uri", labelling)
+        replacement = render_entity(
+            kgcl_instance.has_direct_replacement, "uri", labelling
+        )
+        if kgcl_instance.has_direct_replacement is not None:
+            return "obsolete " + subject + " with replacement " + replacement
+        else:
+            return "obsolete " + subject
+
+    if type(kgcl_instance) is NodeUnobsoletion:
+        subject = render_entity(kgcl_instance.about_node, "uri", labelling)
+        return "unobsolete " + subject
+
+    if type(kgcl_instance) is NodeDeletion:
+        subject = render_entity(kgcl_instance.about_node, "uri", labelling)
+        return "delete " + subject
+
+    if type(kgcl_instance) is NodeMove:
+        subject = render_entity(kgcl_instance.about_edge.subject, "uri", labelling)
+        predicate = render_entity(kgcl_instance.about_edge.predicate, "uri", labelling)
+        new = render_entity(
+            kgcl_instance.new_value, kgcl_instance.new_object_type, labelling
+        )
+        old = render_entity(
+            kgcl_instance.old_value, kgcl_instance.old_object_type, labelling
+        )
+
+        return (
+            "move "
+            + subject
+            + " "
+            + predicate
+            + " "
+            + old
+            + " "
+            + "from"
+            + " "
+            + old
+            + " "
+            + "to"
+            + " "
+            + new
+        )
+
+    if type(kgcl_instance) is EdgeCreation:
+        return render_edge_creation(kgcl_instance, labelling)
+
+    if type(kgcl_instance) is EdgeDeletion:
+        return render_edge_deletion(kgcl_instance, labelling)
+
+    if type(kgcl_instance) is PredicateChange:
+        subject = render_entity(kgcl_instance.about_edge.subject, "uri", labelling)
+        object = render_entity(
+            kgcl_instance.about_edge.object,
+            kgcl_instance.about_edge.object_representation,
+            labelling,
+        )
+
+        new = render_entity(
+            kgcl_instance.new_value,
+            "uri",
+            labelling,
+        )
+        old = render_entity(kgcl_instance.old_value, "uri", labelling)
+
+        if kgcl_instance.language is not None:
+            object += "@" + kgcl_instance.language
+
+        if kgcl_instance.datatype is not None:
+            object += "^^" + kgcl_instance.datatype
+
+        return (
+            "change relationship between "
+            + subject
+            + " and "
+            + object
+            + " from "
+            + old
+            + " to "
+            + new
+        )
+
+    if type(kgcl_instance) is NodeCreation:
+        subject = render_entity(kgcl_instance.about_node, "uri", labelling)
+        label = render_entity(kgcl_instance.name, "label", labelling)
+        if kgcl_instance.name is not None:
+            return "create node " + subject + " " + label
+        else:
+            return "create " + subject
+
+    if type(kgcl_instance) is ClassCreation:
+        subject = render_entity(kgcl_instance.node_id, "uri", labelling)
+        return "create " + subject
+
+    if type(kgcl_instance) is NewSynonym:
+        subject = render_entity(kgcl_instance.about_node, "uri", labelling)
+        synonym = render_entity(kgcl_instance.new_value, "label", labelling)
+        qualifier = kgcl_instance.qualifier
+        language = kgcl_instance.language
+
+        if language is not None:
+            synonym = synonym + "@" + language
+
+        if qualifier is not None:
+            return (
+                "create " + qualifier + " synonym" + " " + synonym + " for " + subject
+            )
+        else:
+            return "create synonym " + synonym + " for " + subject
+
+    if type(kgcl_instance) is PlaceUnder:
+        subclass = render_entity(kgcl_instance.subject, "uri", labelling)
+        superclass = render_entity(kgcl_instance.object, "uri", labelling)
+        return "create edge " + subclass + " rdfs:subClassOf " + superclass
+
+    if type(kgcl_instance) is RemoveUnder:
+        subclass = render_entity(kgcl_instance.subject, "uri", labelling)
+        superclass = render_entity(kgcl_instance.object, "uri", labelling)
+        return "delete edge " + subclass + " rdfs:subClassOf " + superclass
+
+    if type(kgcl_instance) is NodeAnnotationChange:
+        subject = render_entity(
+            kgcl_instance.about_node, kgcl_instance.about_node_representation, labelling
+        )
+        predicate = render_entity(
+            kgcl_instance.annotation_property,
+            kgcl_instance.annotation_property_type,
+            labelling,
+        )
+        old_object = render_entity(
+            kgcl_instance.old_value,
+            kgcl_instance.old_value_type,
+            labelling,
+        )
+        new_object = render_entity(
+            kgcl_instance.new_value,
+            kgcl_instance.new_value_type,
+            labelling,
+        )
+
+        if kgcl_instance.old_language is not None:
+            old_object += "@" + kgcl_instance.old_language
+
+        if kgcl_instance.new_language is not None:
+            new_object += "@" + kgcl_instance.new_language
+
+        if kgcl_instance.old_datatype is not None:
+            old_object += "^^" + kgcl_instance.old_datatype
+
+        if kgcl_instance.new_datatype is not None:
+            new_object += "^^" + kgcl_instance.new_datatype
+
+        return (
+            "change annotation of "
+            + subject
+            + " with "
+            + predicate
+            + " from "
+            + old_object
+            + " to "
+            + new_object
+        )
+
+
+def render_edge_deletion(kgcl_instance, labelling):
+    subject = render_entity(kgcl_instance.subject, "uri", labelling)
+    predicate = render_entity(kgcl_instance.predicate, "uri", labelling)
+    object = render_entity(kgcl_instance.object, kgcl_instance.object_type, labelling)
+
+    language = kgcl_instance.language
+    datatype = kgcl_instance.datatype
+
+    base = "delete edge " + subject + " " + predicate + " " + object
+
+    if language is not None:
+        return base + "@" + language
+    elif datatype is not None:
+        return base + "^^" + datatype
+    else:
+        return base
+
+
+def render_edge_creation(kgcl_instance, labelling):
+    subject = render_entity(kgcl_instance.subject, "uri", labelling)
+    predicate = render_entity(kgcl_instance.predicate, "uri", labelling)
+    object = render_entity(kgcl_instance.object, kgcl_instance.object_type, labelling)
+
+    language = kgcl_instance.language
+    datatype = kgcl_instance.datatype
+
+    base = "create edge " + subject + " " + predicate + " " + object
+
+    if language is not None:
+        return base + "@" + language
+    elif datatype is not None:
+        return base + "^^" + datatype
+    else:
+        return base
diff --git a/kgcl/diff/render_operations.py b/kgcl/diff/render_operations.py
new file mode 100644
index 00000000..57f96d39
--- /dev/null
+++ b/kgcl/diff/render_operations.py
@@ -0,0 +1,228 @@
+from kgcl.model.kgcl import (
+    NodeRename,
+    NodeObsoletion,
+    NodeUnobsoletion,
+    NodeAnnotationChange,
+    NodeDeletion,
+    NodeMove,
+    NodeDeepening,
+    NodeShallowing,
+    EdgeCreation,
+    EdgeDeletion,
+    PredicateChange,
+    NodeCreation,
+    ClassCreation,
+    NewSynonym,
+    PlaceUnder,
+    RemoveUnder,
+    RemovedNodeFromSubset,
+)
+
+
+def render_entity(entity, rdf_type):
+    entity = repr(entity)[1:-1]
+    if rdf_type == "uri":
+        return "<" + entity + ">"
+    elif rdf_type == "label":
+        if "'" in entity:
+            # TODO: replacing quotes with backticks
+            # is only a temporary workaround
+            entity = entity.replace("'", "`")
+        return "'" + entity + "'"
+    elif rdf_type == "literal":
+        # TODO: test this
+        if '"' not in entity:
+            return '"' + entity + '"'
+        elif "'''" not in entity and entity[-1] != "'":
+            return "'''" + entity + "'''"
+        elif '"""' not in entity and entity[-1] != '"':
+            return '"""' + entity + '"""'
+        else:
+            print("Rendering error: " + entity)
+            raise
+    else:
+        print("Rendering error: " + entity)
+        raise
+    # return "'" + entity.replace("\\'", "`") + "'"
+
+
+def render(kgcl_instance):
+
+    if type(kgcl_instance) is NodeRename:
+        # TODO: subject could be 'None'?
+        subject = render_entity(kgcl_instance.about_node, "uri")
+        old = render_entity(kgcl_instance.old_value, "label")
+        new = render_entity(kgcl_instance.new_value, "label")
+
+        new_language = kgcl_instance.new_language
+        old_language = kgcl_instance.old_language
+
+        if old_language is not None:
+            old = old + "@" + old_language
+
+        if new_language is not None:
+            new = new + "@" + new_language
+
+        return "rename " + subject + " from " + old + " to " + new
+
+    if type(kgcl_instance) is NodeAnnotationChange:
+        subject = render_entity(
+            kgcl_instance.about_node, kgcl_instance.about_node_representation
+        )
+        predicate = render_entity(
+            kgcl_instance.annotation_property, kgcl_instance.annotation_property_type
+        )
+        old_object = render_entity(
+            kgcl_instance.old_value, kgcl_instance.old_value_type
+        )
+        new_object = render_entity(
+            kgcl_instance.new_value, kgcl_instance.new_value_type
+        )
+
+        if kgcl_instance.old_language is not None:
+            old_object += "@" + kgcl_instance.old_language
+
+        if kgcl_instance.new_language is not None:
+            new_object += "@" + kgcl_instance.new_language
+
+        if kgcl_instance.old_datatype is not None:
+            old_object += "^^" + kgcl_instance.old_datatype
+
+        if kgcl_instance.new_datatype is not None:
+            new_object += "^^" + kgcl_instance.new_datatype
+
+        return (
+            "change annotation of "
+            + subject
+            + " with "
+            + predicate
+            + " from "
+            + old_object
+            + " to "
+            + new_object
+        )
+
+    if type(kgcl_instance) is NodeObsoletion:
+        subject = render_entity(kgcl_instance.about_node, "uri")
+        # TODO: type this correctly
+        replacement = render_entity(kgcl_instance.has_direct_replacement, "uri")
+        if kgcl_instance.has_direct_replacement is not None:
+            return "obsolete " + subject + " with replacement " + replacement
+        else:
+            return "obsolete " + subject
+
+    if type(kgcl_instance) is NodeUnobsoletion:
+        subject = render_entity(kgcl_instance.about_node, "uri")
+        return "unobsolete " + subject
+
+    if type(kgcl_instance) is NodeDeletion:
+        subject = render_entity(kgcl_instance.about_node, "uri")
+        return "delete " + subject
+
+    if type(kgcl_instance) is NodeMove:
+        subject = render_entity(kgcl_instance.about_edge.subject, "uri")
+        predicate = render_entity(kgcl_instance.about_edge.predicate, "uri")
+        new = render_entity(kgcl_instance.new_value, kgcl_instance.new_object_type)
+        old = render_entity(kgcl_instance.old_value, kgcl_instance.old_object_type)
+        return (
+            "move "
+            + subject
+            + " "
+            + predicate
+            + " "
+            + old
+            + " "
+            + "from"
+            + " "
+            + old
+            + " "
+            + "to"
+            + " "
+            + new
+        )
+
+    if type(kgcl_instance) is PredicateChange:
+        subject = render_entity(kgcl_instance.about_edge.subject, "uri")
+        object = render_entity(
+            kgcl_instance.about_edge.object, kgcl_instance.object_type
+        )
+        new = render_entity(kgcl_instance.new_value, "uri")
+        old = render_entity(kgcl_instance.old_value, "uri")
+
+        if kgcl_instance.language is not None:
+            object += "@" + kgcl_instance.language
+
+        if kgcl_instance.datatype is not None:
+            object += "^^" + kgcl_instance.datatype
+
+        return (
+            "change relationship between "
+            + subject
+            + " and "
+            + object
+            + " from "
+            + old
+            + " to "
+            + new
+        )
+
+    if type(kgcl_instance) is NodeCreation:
+        subject = render_entity(kgcl_instance.about_node, "uri")
+        label = render_entity(kgcl_instance.name, "label")
+        if kgcl_instance.name is not None:
+            return "create node " + subject + " " + label
+        else:
+            return "create " + subject
+
+    if type(kgcl_instance) is ClassCreation:
+        subject = render_entity(kgcl_instance.about_node, "uri")
+        return "create " + subject
+
+    if type(kgcl_instance) is NewSynonym:
+        subject = render_entity(kgcl_instance.about_node, "uri")
+        synonym = render_entity(kgcl_instance.new_value, "label")
+        qualifier = kgcl_instance.qualifier
+        language = kgcl_instance.language
+
+        if language is not None:
+            synonym = synonym + "@" + language
+
+        if qualifier is not None:
+            return (
+                "create " + qualifier + " synonym" + " " + synonym + " for " + subject
+            )
+        else:
+            return "create synonym " + synonym + " for " + subject
+
+    if type(kgcl_instance) is PlaceUnder:
+        subclass = render_entity(kgcl_instance.subject, "uri")
+        superclass = render_entity(kgcl_instance.object, "uri")
+        return (
+            "create edge "
+            + subclass
+            + " <http://www.w3.org/2000/01/rdf-schema#subClassOf> "
+            + superclass
+        )
+
+    if type(kgcl_instance) is RemoveUnder:
+        subclass = render_entity(kgcl_instance.subject, "uri")
+        superclass = render_entity(kgcl_instance.object, "uri")
+        return (
+            "delete edge "
+            + subclass
+            + " <http://www.w3.org/2000/01/rdf-schema#subClassOf> "
+            + superclass
+        )
+
+    # TODO these need to be changed
+    if type(kgcl_instance) is EdgeCreation:
+        subclass = render_entity(kgcl_instance.subject, "uri")
+        property = render_entity(kgcl_instance.predicate, "uri")
+        filler = render_entity(kgcl_instance.object, "uri")
+        return "create edge " + subclass + " " + property + " " + filler
+
+    if type(kgcl_instance) is EdgeDeletion:
+        subclass = render_entity(kgcl_instance.subject, "uri")
+        property = render_entity(kgcl_instance.predicate, "uri")
+        filler = render_entity(kgcl_instance.object, "uri")
+        return "delete edge " + subclass + " " + property + " " + filler
diff --git a/kgcl/diff/summary_generation.py b/kgcl/diff/summary_generation.py
new file mode 100644
index 00000000..45b6e92f
--- /dev/null
+++ b/kgcl/diff/summary_generation.py
@@ -0,0 +1,152 @@
+import rdflib
+from datetime import datetime
+from kgcl.diff.pretty_print_kgcl import render_instances
+import kgcl.diff.diff_2_kgcl_single as single
+import kgcl.diff.diff_2_kgcl_existential as existential
+import os
+from rdflib.util import guess_format
+
+
+def ts():
+    now = datetime.now()
+    dt_string = now.strftime("%H:%M:%S ")
+    return dt_string
+
+
+def run(ingraph, outgraph, output):
+
+    os.mkdir(output)
+
+    # load graphs
+    g1 = rdflib.Graph()
+    g2 = rdflib.Graph()
+    # g1.parse(ingraph)
+    g1.load(ingraph, format=guess_format(ingraph))
+    print(ts() + "Loaded Graph 1")
+    # g2.parse(outgraph)
+    g2.load(outgraph, format=guess_format(outgraph))
+    print(ts() + "Loaded Graph 2")
+
+    # compute diff
+    existential_summary = existential.generate_atomic_existential_commands(g1, g2)
+    print(ts() + "Generated Diff for Existentials")
+    single_triple_summary = single.generate_thin_triple_commands(g1, g2)
+    print(ts() + "Generated Diff for Thin Triples")
+
+    # write summary report
+    with open(output + "/kgcl_summary.txt", "w") as f:
+        f.write(existential_summary.get_summary_KGCL_commands())
+        f.write(single_triple_summary.get_summary_KGCL_commands())
+
+    # write non-deterministic diff report
+    nd_node_moves = single_triple_summary.get_non_deterministic_node_moves()
+    nd_predicate_changes = (
+        single_triple_summary.get_non_deterministic_predicate_changes()
+    )
+    nd_renamings = single_triple_summary.get_non_deterministic_renamings()
+    nd_annotation_changes = (
+        single_triple_summary.get_non_deterministic_annotation_changes()
+    )
+
+    non_deterministic_folder = os.path.join(output, "non_deterministic")
+    os.mkdir(non_deterministic_folder)
+    with open(non_deterministic_folder + "/node_moves.txt", "w") as f:
+        f.write(render_non_deterministic_diff(nd_node_moves))
+    with open(non_deterministic_folder + "/predicate_changes.txt", "w") as f:
+        f.write(render_non_deterministic_diff(nd_predicate_changes))
+    with open(non_deterministic_folder + "/renamings.txt", "w") as f:
+        f.write(render_non_deterministic_diff(nd_renamings))
+    with open(non_deterministic_folder + "/annotation_changes.txt", "w") as f:
+        f.write(render_non_deterministic_diff(nd_annotation_changes))
+
+    # get KGCL commands
+    kgcl_commands = existential_summary.get_commands()
+    kgcl_commands += single_triple_summary.get_commands()
+
+    # write KGCL commands
+    with open(output + "/patch.kgcl", "w") as f:
+        for k in kgcl_commands:
+            f.write(k)
+            f.write("\n")
+
+    # write distinct KGCL commands
+    patch_folder = os.path.join(output, "patch")
+    os.mkdir(patch_folder)
+
+    with open(patch_folder + "/existential_additions.txt", "w") as f:
+        for k in existential_summary.get_existential_additions():
+            f.write(k)
+            f.write("\n")
+
+    with open(patch_folder + "/existential_deletions.txt", "w") as f:
+        for k in existential_summary.get_existential_deletions():
+            f.write(k)
+            f.write("\n")
+
+    with open(patch_folder + "/renamings.txt", "w") as f:
+        for k in single_triple_summary.get_renamings():
+            f.write(k)
+            f.write("\n")
+
+    with open(patch_folder + "/class_creations.txt", "w") as f:
+        for k in single_triple_summary.get_class_creations():
+            f.write(k)
+            f.write("\n")
+
+    with open(patch_folder + "/subsumption_creations.txt", "w") as f:
+        for k in single_triple_summary.get_subsumption_creations():
+            f.write(k)
+            f.write("\n")
+
+    with open(patch_folder + "/subsumption_deletions.txt", "w") as f:
+        for k in single_triple_summary.get_subsumption_deletions():
+            f.write(k)
+            f.write("\n")
+
+    with open(patch_folder + "/predicate_changes.txt", "w") as f:
+        for k in single_triple_summary.get_predicate_changes():
+            f.write(k)
+            f.write("\n")
+
+    with open(patch_folder + "/node_moves.txt", "w") as f:
+        for k in single_triple_summary.get_node_moves():
+            f.write(k)
+            f.write("\n")
+
+    with open(patch_folder + "/synonym_creations.txt", "w") as f:
+        for k in single_triple_summary.get_synonym_creations():
+            f.write(k)
+            f.write("\n")
+
+    with open(patch_folder + "/node_annotation_changes.txt", "w") as f:
+        for k in single_triple_summary.get_annotation_changes():
+            f.write(k)
+            f.write("\n")
+
+    # TODO pretty printing
+    pp_kgcl_commands = render_instances(kgcl_commands, g1)
+    with open(output + "/pp_patch.kgcl", "w") as f:
+        for a in pp_kgcl_commands:
+            f.write(a)
+            f.write("\n")
+
+
+# a non-deterministic diff consists of a list of tuples.
+# A tuple contains two sets of triples that are involved in the non-deterministic diff
+def render_non_deterministic_diff(nd):
+    out = ""
+    for tuple in nd:
+        from_triples = tuple[0]
+        to_triples = tuple[1]
+        rendering = ""
+        for s, p, o in from_triples:
+            triple = " (" + str(s) + "," + str(p) + "," + str(o) + ")\n"
+            rendering += triple
+        rendering += "->\n"
+        for s, p, o in to_triples:
+            triple = " (" + str(s) + "," + str(p) + "," + str(o) + ")\n"
+            rendering += triple
+
+        out += rendering + "\n\n"
+
+    return out
diff --git a/kgcl/grammar/__init__.py b/kgcl/grammar/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/kgcl/grammar/__pycache__/__init__.cpython-36.pyc b/kgcl/grammar/__pycache__/__init__.cpython-36.pyc
new file mode 100644
index 00000000..bb422d81
Binary files /dev/null and b/kgcl/grammar/__pycache__/__init__.cpython-36.pyc differ
diff --git a/kgcl/grammar/__pycache__/parser.cpython-36.pyc b/kgcl/grammar/__pycache__/parser.cpython-36.pyc
new file mode 100644
index 00000000..46d4c30a
Binary files /dev/null and b/kgcl/grammar/__pycache__/parser.cpython-36.pyc differ
diff --git a/kgcl/grammar/kgcl.lark b/kgcl/grammar/kgcl.lark
new file mode 100644
index 00000000..91e01469
--- /dev/null
+++ b/kgcl/grammar/kgcl.lark
@@ -0,0 +1,131 @@
+?expression: rename 
+           | create
+           | obsolete 
+           | delete 
+           | move
+           | create_class 
+           | unobsolete
+           | deepen
+           | shallow
+           | change_relationship
+           | change_annotation
+           | create_edge
+           | delete_edge 
+           | create_synonym 
+           | remove_from_subset
+           | add_to_subset 
+
+rename : "rename" _WS [id _WS "from" _WS] old_label ["@" old_language] _WS ("to"|"as"|"->") _WS new_label ["@" new_language]
+
+create : "create node" _WS id _WS label ["@" language]
+create_class: "create" _WS id
+
+create_synonym: "create" _WS [synonym_qualifier _WS] "synonym" _WS synonym ["@" language] _WS "for" _WS entity
+synonym_qualifier : /exact|narrow|broad|related/
+
+delete : "delete" _WS entity
+
+obsolete : "obsolete" _WS entity
+         | "obsolete" _WS entity _WS "with replacement" _WS replacement
+
+unobsolete: "unobsolete" _WS entity 
+
+deepen : "deepen" _WS entity _WS "from" _WS old_entity _WS ("to"|"->") _WS new_entity 
+shallow : "shallow" _WS entity _WS "from" _WS old_entity _WS ("to"|"->") _WS new_entity 
+
+move : "move" _WS entity_subject _WS entity_predicate _WS entity_object _WS "from" _WS old_entity _WS ("to"|"as"|"->") _WS new_entity 
+
+create_edge : "create edge" _WS entity_subject _WS entity_predicate _WS entity_object_id
+delete_edge : "delete edge" _WS entity_subject _WS entity_predicate _WS entity_object_id 
+
+change_relationship: "change relationship between" _WS entity_subject _WS "and" _WS entity_object _WS "from" _WS old_entity _WS "to" _WS new_entity 
+
+change_annotation: "change annotation of" _WS entity_subject _WS "with" _WS entity_predicate _WS "from" _WS old_entity_object _WS "to" _WS new_entity_object
+
+
+remove_from_subset :  "remove" _WS id _WS "from subset" _WS subset
+add_to_subset :  "add" _WS id _WS "to subset" _WS subset
+
+replacement : ID
+id : ID 
+     | CURIE
+
+subset: ID
+synonym : LABEL
+
+subclass: _entity
+superclass: _entity
+property: _entity
+filler: _entity
+
+label : SINGLE_QUOTE_LITERAL
+old_label : SINGLE_QUOTE_LITERAL
+new_label : SINGLE_QUOTE_LITERAL
+
+language: LANGUAGE_TAG
+old_language : LANGUAGE_TAG
+new_language : LANGUAGE_TAG
+datatype: _datatype
+new_datatype: _datatype
+old_datatype: _datatype
+
+_datatype: "^^" ID
+
+IRI: /((?!>>)\S)+/
+     | /((?!>>>)\S)+/
+
+LANGUAGE_TAG :  /[a-zA-Z]+(?:-[a-zA-Z0-9]+)*/
+
+annotation_property : _entity
+annotation : STRING
+
+old_id : ID
+new_id : ID
+
+old_predicate : ID
+new_predicate : ID
+
+entity: _entity 
+old_entity: _entity
+new_entity: _entity
+entity_subject: _entity
+entity_predicate: _entity
+entity_object_id: _entity
+entity_object: _entity
+              | TERM ["@" language | datatype] 
+old_entity_object: _entity
+              | TERM ["@" old_language | old_datatype] 
+new_entity_object: _entity
+              | TERM ["@" new_language | new_datatype] 
+
+_entity: ID
+         | LABEL 
+         | CURIE
+
+CURIE: /[a-zA-Z]+/ ":" /((?!>>)\S)+/
+
+LABEL: SINGLE_QUOTE_LITERAL
+
+TERM : TRIPLE_SINGLE_QUOTE_LITERAL
+       | TRIPLE_DOUBLE_QUOTE_LITERAL
+       | DOUBLE_QUOTE_LITERAL
+
+DOUBLE_QUOTE_LITERAL : "\"" INNER_DOUBLE_QUOTE_LITERAL "\""
+TRIPLE_DOUBLE_QUOTE_LITERAL : "\"\"\"" INNER_TRIPLE_DOUBLE_QUOTE_LITERAL "\"\"\""
+
+TRIPLE_SINGLE_QUOTE_LITERAL : "'''" INNER_TRIPLE_SINGLE_QUOTE_LITERAL "'''" 
+SINGLE_QUOTE_LITERAL : "'" INNER_SINGLE_QUOTE_LITERAL "'" 
+
+INNER_DOUBLE_QUOTE_LITERAL: /((?!\").)*/
+INNER_TRIPLE_DOUBLE_QUOTE_LITERAL: /((?!\"\"\").)*/
+
+INNER_SINGLE_QUOTE_LITERAL: /((?!\').)*/
+INNER_TRIPLE_SINGLE_QUOTE_LITERAL: /((?!\'\'\').)*/
+
+ID :  "<" INNER_ID ">" 
+
+INNER_ID: /((?!>).)*/
+STRING: /.+/
+
+_WS: /[ \t\f\r\n]/+
+
diff --git a/kgcl/grammar/kgcl_2_rdf.py b/kgcl/grammar/kgcl_2_rdf.py
new file mode 100644
index 00000000..442a11c7
--- /dev/null
+++ b/kgcl/grammar/kgcl_2_rdf.py
@@ -0,0 +1,493 @@
+from kgcl.grammar.parser import parse
+from kgcl.utils import from_yaml, to_rdf, to_json
+from kgcl.model.kgcl import (
+    NodeRename,
+    NodeObsoletion,
+    NodeUnobsoletion,
+    NodeDeletion,
+    NodeAnnotationChange,
+    NodeMove,
+    NodeDeepening,
+    NodeShallowing,
+    EdgeCreation,
+    EdgeDeletion,
+    PredicateChange,
+    NodeCreation,
+    ClassCreation,
+    NewSynonym,
+    RemovedNodeFromSubset,
+    PlaceUnder,
+    RemoveUnder,
+)
+
+# parse kgcl to python data class
+# serialse puython data class to yaml
+# convert python data class to RDF
+
+
+def kgcl_2_yaml(kgcl_patch, output):
+    parsed_patch = parse(kgcl_patch)
+    serialise_yaml(parsed_patch, output)
+
+
+def serialise_yaml(kgcl_instances, output):
+
+    preamble = """activity_set:
+- {id: uuid:876aac6a6ef6, description: "my sane edits", was_associated_with: "https://orcid.org/0000-0002-6601-2165"}
+- {id: uuid:e887d3aac33f, description: "deliberate mistakes", was_associated_with: "OntologicalAnarchist"}
+change_set:\n"""
+
+    with open(output, "w") as f:
+        f.write(preamble)
+        for k in kgcl_instances:
+            f.write(data_model_to_yaml(k))
+            f.write("\n")
+
+
+def data_model_to_yaml(kgcl_instance):
+    if type(kgcl_instance) is NodeRename:
+        return rename(kgcl_instance)
+    if type(kgcl_instance) is NodeObsoletion:
+        return obsoletion(kgcl_instance)
+    if type(kgcl_instance) is NodeUnobsoletion:
+        return unobsoletion(kgcl_instance)
+    if type(kgcl_instance) is NodeDeletion:
+        return node_deletion(kgcl_instance)
+    if type(kgcl_instance) is NodeMove:
+        return node_move(kgcl_instance)
+    if type(kgcl_instance) is NodeDeepening:
+        return node_deepening(kgcl_instance)
+    if type(kgcl_instance) is NodeShallowing:
+        return node_shallowing(kgcl_instance)
+    if type(kgcl_instance) is EdgeCreation:
+        return edge_creation(kgcl_instance)
+    if type(kgcl_instance) is EdgeDeletion:
+        return edge_deletion(kgcl_instance)
+    if type(kgcl_instance) is PredicateChange:
+        return predicate_change(kgcl_instance)
+    if type(kgcl_instance) is NodeAnnotationChange:
+        return node_annotation_change(kgcl_instance)
+    if type(kgcl_instance) is NodeCreation:
+        return node_creation(kgcl_instance)
+    if type(kgcl_instance) is ClassCreation:
+        return class_creation(kgcl_instance)
+    if type(kgcl_instance) is NewSynonym:
+        return new_synonym(kgcl_instance)
+    if type(kgcl_instance) is PlaceUnder:
+        return place_under(kgcl_instance)
+    if type(kgcl_instance) is RemoveUnder:
+        return place_under(kgcl_instance)
+
+
+def rename(kgcl_instance):
+    type = "NodeRename"
+    id = kgcl_instance.id
+    about_node = render_id(
+        kgcl_instance.about_node, kgcl_instance.about_node_representation
+    )
+    old_value = kgcl_instance.old_value
+    new_value = kgcl_instance.new_value
+    old_language = kgcl_instance.old_language
+    new_language = kgcl_instance.new_language
+
+    serialisation = (
+        f"type: {type}, "
+        f"id: {id}, "
+        f"about_node: {about_node}, "
+        f"old_value: {old_value}, "
+        f"new_value: {new_value}, "
+        f"old_language: {old_language}, "
+        f"new_language: {new_language}"
+    )
+
+    return "- {" + serialisation + "}"
+
+
+def obsoletion(kgcl_instance):
+    type = "NodeObsoletion"
+    id = kgcl_instance.id
+    about_node = render_id(
+        kgcl_instance.about_node, kgcl_instance.about_node_representation
+    )
+    replacement = kgcl_instance.has_direct_replacement
+
+    serialisation = (
+        f"type: {type}, "
+        f"id: {id}, "
+        f"about_node: {about_node}, "
+        f"has_direct_replacement: {replacement} "
+    )
+
+    return "- {" + serialisation + "}"
+
+
+def unobsoletion(kgcl_instance):
+    type = "NodeUnobsoletion"
+    id = kgcl_instance.id
+    about_node = render_id(
+        kgcl_instance.about_node, kgcl_instance.about_node_representation
+    )
+
+    serialisation = f"type: {type}, " f"id: {id}, " f"about_node: {about_node}"
+
+    return "- {" + serialisation + "}"
+
+
+def node_deletion(kgcl_instance):
+    type = "NodeDeletion"
+    id = kgcl_instance.id
+    about_node = render_id(
+        kgcl_instance.about_node, kgcl_instance.about_node_representation
+    )
+
+    serialisation = f"type: {type}, " f"id: {id}, " f"about_node: {about_node}"
+
+    return "- {" + serialisation + "}"
+
+
+def node_move(kgcl_instance):
+    type = "NodeMove"
+    id = kgcl_instance.id
+    subject = render_id(
+        kgcl_instance.about_edge.subject,
+        kgcl_instance.about_edge.subject_representation,
+    )
+    predicate = render_id(
+        kgcl_instance.about_edge.predicate,
+        kgcl_instance.about_edge.predicate_representation,
+    )
+    object = render_id(
+        kgcl_instance.about_edge.object,
+        kgcl_instance.about_edge.object_representation,
+    )
+    about_edge = (
+        f"{{"
+        f"subject: {subject}, "
+        f"predicate: {predicate}, "
+        f"object: {object} "
+        f"}}"
+    )
+
+    new_value = render_id(kgcl_instance.new_value, kgcl_instance.new_object_type)
+    old_value = render_id(kgcl_instance.old_value, kgcl_instance.old_object_type)
+
+    serialisation = (
+        f"type: {type}, "
+        f"id: {id}, "
+        f"about_edge: {about_edge},"
+        f"new_value: {new_value}, "
+        f"old_value: {old_value} "
+    )
+
+    return "- {" + serialisation + "}"
+
+
+def node_deepening(kgcl_instance):
+    type = "NodeDeepening"
+    id = kgcl_instance.id
+    subject = render_id(
+        kgcl_instance.about_edge.subject,
+        kgcl_instance.about_edge.subject_representation,
+    )
+    predicate = render_id(
+        kgcl_instance.about_edge.predicate,
+        kgcl_instance.about_edge.predicate_representation,
+    )
+    object = render_id(
+        kgcl_instance.about_edge.object,
+        kgcl_instance.about_edge.object_representation,
+    )
+    about_edge = (
+        f"{{"
+        f"subject: {subject}, "
+        f"predicate: {predicate}, "
+        f"object: {object} "
+        f"}}"
+    )
+
+    new_value = render_id(kgcl_instance.new_value, kgcl_instance.new_object_type)
+    old_value = render_id(kgcl_instance.old_value, kgcl_instance.old_object_type)
+
+    serialisation = (
+        f"type: {type}, "
+        f"id: {id}, "
+        f"about_edge: {about_edge},"
+        f"new_value: {new_value}, "
+        f"old_value: {old_value} "
+    )
+
+    return "- {" + serialisation + "}"
+
+
+def node_shallowing(kgcl_instance):
+    type = "NodeShallowing"
+    id = kgcl_instance.id
+    subject = render_id(
+        kgcl_instance.about_edge.subject,
+        kgcl_instance.about_edge.subject_representation,
+    )
+    predicate = render_id(
+        kgcl_instance.about_edge.predicate,
+        kgcl_instance.about_edge.predicate_representation,
+    )
+    object = render_id(
+        kgcl_instance.about_edge.object,
+        kgcl_instance.about_edge.object_representation,
+    )
+    about_edge = (
+        f"{{"
+        f"subject: {subject}, "
+        f"predicate: {predicate}, "
+        f"object: {object} "
+        f"}}"
+    )
+
+    new_value = render_id(kgcl_instance.new_value, kgcl_instance.new_object_type)
+    old_value = render_id(kgcl_instance.old_value, kgcl_instance.old_object_type)
+
+    serialisation = (
+        f"type: {type}, "
+        f"id: {id}, "
+        f"about_edge: {about_edge},"
+        f"new_value: {new_value}, "
+        f"old_value: {old_value} "
+    )
+
+    return "- {" + serialisation + "}"
+
+
+def edge_creation(kgcl_instance):
+    type = "EdgeCreation"
+    id = kgcl_instance.id
+    subject = render_id(
+        kgcl_instance.subject,
+        kgcl_instance.subject_type,
+    )
+    predicate = render_id(
+        kgcl_instance.predicate,
+        kgcl_instance.predicate_type,
+    )
+    object = render_id(
+        kgcl_instance.object,
+        kgcl_instance.object_type,
+    )
+
+    serialisation = (
+        f"type: {type}, "
+        f"id: {id}, "
+        f"subject: {subject}, "
+        f"predicate: {predicate}, "
+        f"object: {object} "
+    )
+
+    return "- {" + serialisation + "}"
+
+
+def edge_deletion(kgcl_instance):
+    type = "EdgeDeletion"
+    id = kgcl_instance.id
+    subject = render_id(
+        kgcl_instance.subject,
+        kgcl_instance.subject_type,
+    )
+    predicate = render_id(
+        kgcl_instance.predicate,
+        kgcl_instance.predicate_type,
+    )
+    object = render_id(
+        kgcl_instance.object,
+        kgcl_instance.object_type,
+    )
+
+    serialisation = (
+        f"type: {type}, "
+        f"id: {id}, "
+        f"subject: {subject}, "
+        f"predicate: {predicate}, "
+        f"object: {object} "
+    )
+
+    return "- {" + serialisation + "}"
+
+
+def predicate_change(kgcl_instance):
+    type = "PredicateChange"
+    id = kgcl_instance.id
+    subject = render_id(
+        kgcl_instance.about_edge.subject,
+        kgcl_instance.about_edge.subject_representation,
+    )
+    predicate = render_id(
+        kgcl_instance.about_edge.predicate,
+        kgcl_instance.about_edge.predicate_representation,
+    )
+    object = render_id(
+        kgcl_instance.about_edge.object,
+        kgcl_instance.about_edge.object_representation,
+    )
+    about_edge = (
+        f"{{"
+        f"subject: {subject}, "
+        f"predicate: {predicate}, "
+        f"object: {object} "
+        f"}}"
+    )
+    old_value = kgcl_instance.old_value
+    new_value = kgcl_instance.new_value
+    old_language = kgcl_instance.old_language
+    new_language = kgcl_instance.new_language
+
+    serialisation = (
+        f"type: {type}, "
+        f"id: {id}, "
+        f"about_edge: {about_edge},"
+        f"old_value: {old_value}, "
+        f"new_value: {new_value}, "
+        f"old_language: {old_language}, "
+        f"new_language: {new_language}"
+    )
+
+    return "- {" + serialisation + "}"
+
+
+def node_annotation_change(kgcl_instance):
+    type = "NodeAnnotationChange"
+    id = kgcl_instance.id
+    about_node = render_id(
+        kgcl_instance.about_node, kgcl_instance.about_node_representation
+    )
+    old_value = kgcl_instance.old_value
+    new_value = kgcl_instance.new_value
+    old_language = kgcl_instance.old_language
+    new_language = kgcl_instance.new_language
+    old_datatype = kgcl_instance.old_datatype
+    new_datatype = kgcl_instance.new_datatype
+
+    serialisation = (
+        f"type: {type}, "
+        f"id: {id}, "
+        f"about_node: {about_node},"
+        f"old_value: {old_value}, "
+        f"new_value: {new_value}, "
+        f"old_language: {old_language}, "
+        f"new_language: {new_language},"
+        f"old_datatype: {old_datatype}, "
+        f"new_datatype: {new_datatype}"
+    )
+
+    return "- {" + serialisation + "}"
+
+
+def node_creation(kgcl_instance):
+    type = "NodeCreation"
+    id = kgcl_instance.id
+    about_node = render_id(
+        kgcl_instance.about_node, kgcl_instance.about_node_representation
+    )
+    name = kgcl_instance.name
+    language = kgcl_instance.language
+
+    serialisation = (
+        f"type: {type}, "
+        f"id: {id}, "
+        f"about_node: {about_node}, "
+        f"language: {language},"
+        f"name: {name}"
+    )
+
+    return "- {" + serialisation + "}"
+
+
+def class_creation(kgcl_instance):
+    type = "ClassCreation"
+    id = kgcl_instance.id
+    about_node = render_id(
+        kgcl_instance.about_node, kgcl_instance.about_node_representation
+    )
+
+    serialisation = f"type: {type}, " f"id: {id}, " f"about_node: {about_node}, "
+
+    return "- {" + serialisation + "}"
+
+
+def new_synonym(kgcl_instance):
+    type = "NewSynonym"
+    id = kgcl_instance.id
+    about_node = render_id(
+        kgcl_instance.about_node, kgcl_instance.about_node_representation
+    )
+    new_value = kgcl_instance.new_value
+    qualifier = kgcl_instance.qualifier
+    language = kgcl_instance.language
+
+    serialisation = (
+        f"type: {type}, "
+        f"id: {id}, "
+        f"about_node: {about_node}, "
+        f"language: {language},"
+        f"new_value: {new_value},"
+        f"qualifier: {qualifier}"
+    )
+
+    return "- {" + serialisation + "}"
+
+
+def place_under(kgcl_instance):
+    type = "PlaceUnder"
+    id = kgcl_instance.id
+    subject = render_id(
+        kgcl_instance.subject,
+        kgcl_instance.subject_type,
+    )
+    predicate = render_id(
+        kgcl_instance.predicate,
+        kgcl_instance.predicate_type,
+    )
+    object = render_id(
+        kgcl_instance.object,
+        kgcl_instance.object_type,
+    )
+
+    serialisation = (
+        f"type: {type}, "
+        f"id: {id}, "
+        f"subject: {subject}, "
+        f"predicate: {predicate}, "
+        f"object: {object} "
+    )
+
+    return "- {" + serialisation + "}"
+
+
+def remove_under(kgcl_instance):
+    type = "RemoveUnder"
+    id = kgcl_instance.id
+    subject = render_id(
+        kgcl_instance.subject,
+        kgcl_instance.subject_type,
+    )
+    predicate = render_id(
+        kgcl_instance.predicate,
+        kgcl_instance.predicate_type,
+    )
+    object = render_id(
+        kgcl_instance.object,
+        kgcl_instance.object_type,
+    )
+
+    serialisation = (
+        f"type: {type}, "
+        f"id: {id}, "
+        f"subject: {subject}, "
+        f"predicate: {predicate}, "
+        f"object: {object} "
+    )
+
+    return "- {" + serialisation + "}"
+
+
+def render_id(id, type):
+    if type == "label":
+        return '"' + str(id) + '"'
+    else:
+        return str(id)
diff --git a/kgcl/grammar/parser.py b/kgcl/grammar/parser.py
new file mode 100644
index 00000000..1139c4b5
--- /dev/null
+++ b/kgcl/grammar/parser.py
@@ -0,0 +1,518 @@
+from lark import Lark, Token
+from kgcl.model.kgcl import (
+    NodeRename,
+    NodeObsoletion,
+    NodeUnobsoletion,
+    NodeDeletion,
+    NodeMove,
+    NodeDeepening,
+    NodeShallowing,
+    EdgeCreation,
+    EdgeDeletion,
+    PredicateChange,
+    NodeAnnotationChange,
+    NodeCreation,
+    ClassCreation,
+    NewSynonym,
+    RemovedNodeFromSubset,
+    PlaceUnder,
+    RemoveUnder,
+)
+from kgcl.model.ontology_model import Edge, Annotation
+from pathlib import Path
+
+
+def id_generator():
+    """
+    Returns a new ID for KGCL change operations.
+    """
+    id = 0
+    while True:
+        yield id
+        id += 1
+
+
+# initialise ID generator
+id_gen = id_generator()
+
+
+# initialise parser
+path = Path(__file__).parent
+kgcl_parser = Lark.open(str(path) + "/kgcl.lark", start="expression")
+
+
+def parse(input):
+    """
+    Parse a set of KGCL command separated by \n and
+    return instantiated dataclass objects from model.kgcl.
+    """
+    statements = input.splitlines()
+    parsed = []
+
+    for s in statements:
+        parsed.append(parse_statement(s))
+    return parsed
+
+
+def parse_statement(input):
+    """
+    Parse a KGCL command and
+    return an instantiated dataclass object from model.kgcl.
+    """
+
+    tree = kgcl_parser.parse(input)
+    id = "kgcl_change_id_" + str(next(id_gen))
+
+    command = tree.data
+
+    if command == "rename":
+        return parse_rename(tree, id)
+    elif command == "obsolete":
+        return parse_obsolete(tree, id)
+    elif command == "unobsolete":
+        return parse_unobsolete(tree, id)
+    elif command == "delete":
+        return parse_delete(tree, id)
+    elif command == "move":
+        return parse_move(tree, id)
+    elif command == "deepen":
+        return parse_deepen(tree, id)
+    elif command == "shallow":
+        return parse_shallow(tree, id)
+    elif command == "create_edge":
+        return parse_create_edge(tree, id)
+    elif command == "delete_edge":
+        return parse_delete_edge(tree, id)
+    elif command == "change_relationship":
+        return parse_change_relationship(tree, id)
+    elif command == "change_annotation":
+        return parse_change_annotation(tree, id)
+    elif command == "create":
+        return parse_create(tree, id)
+    elif command == "create_class":
+        return parse_create_class(tree, id)
+    elif command == "create_synonym":
+        return parse_create_synonym(tree, id)
+    elif command == "remove_from_subset":
+        return parse_remove_from_subset(tree, id)
+    else:
+        raise NotImplementedError("No implementation for KGCL command: " + command)
+
+    # TODO: does not have a field for subsets
+    # if(command == "add_to_subset"):
+
+    #    term_id = next(tree.find_data('id'))
+    #    term_id_token = next(get_tokens(term_id))
+
+    #    subset_id = next(tree.find_data('subset'))
+    #    subset_id_token = next(get_tokens(subset_id))
+
+    #    return AddNodeToSubset(
+    #        id=id, in_subset=subset_id_token, about_node=term_id_token
+    #     )
+
+    # TODO: more commands
+    # if(command == "merge"):
+    # if(command == "add"):
+    # if(command == "add_parent"):
+    # if(command == "add_annotation"):
+    # if(command == "add_axiom"): (in Manchester Syntax?)
+    # if(command == "add_class"):
+    # if(command == "add_text_definition"):
+    # if(command == "merge"):
+
+
+def parse_remove_from_subset(tree, id):
+    term_id_token = extract(tree, "id")
+    subset_id_token = extract(tree, "subset")
+
+    return RemovedNodeFromSubset(
+        id=id, subset=subset_id_token, about_node=term_id_token
+    )
+
+
+def parse_create_synonym(tree, id):
+    entity_token = extract(tree, "entity")
+    entity, representation = get_entity_representation(entity_token)
+
+    synonym_string_token = extract(tree, "synonym")
+    synonym, representation2 = get_entity_representation(synonym_string_token)
+
+    language_token = extract(tree, "language")
+    qualifier_token = extract(tree, "synonym_qualifier")
+
+    return NewSynonym(
+        id=id,
+        about_node=entity,
+        about_node_representation=representation,
+        new_value=synonym,
+        qualifier=qualifier_token,
+        language=language_token,
+    )
+
+
+def parse_create_class(tree, id):
+    term_id_token = extract(tree, "id")
+    entity, representation = get_entity_representation(term_id_token)
+
+    return ClassCreation(
+        id=id, node_id=entity, about_node_representation=representation
+    )
+
+
+# the KGCL model suggests the command
+# 'create node {id} {label} with {annotation set}'
+# TODO: handling of {annotation set}
+def parse_create(tree, id):
+    term_id_token = extract(tree, "id")
+    label_token = extract(tree, "label")
+    language_token = extract(tree, "language")
+
+    entity, representation = get_entity_representation(term_id_token)
+
+    return NodeCreation(
+        id=id,
+        about_node=entity,
+        about_node_representation=representation,
+        node_id=term_id_token,
+        name=label_token,
+        language=language_token,
+    )
+
+
+def parse_change_annotation(tree, id):
+    subject_token = extract(tree, "entity_subject")
+    predicate_token = extract(tree, "entity_predicate")
+
+    subject, s_representation = get_entity_representation(subject_token)
+    predicate, p_representation = get_entity_representation(predicate_token)
+
+    old_token = extract(tree, "old_entity_object")
+    new_token = extract(tree, "new_entity_object")
+
+    old, old_representation = get_entity_representation(old_token)
+    new, new_representation = get_entity_representation(new_token)
+
+    old_language_token = extract(tree, "old_language")
+    new_language_token = extract(tree, "new_language")
+
+    old_datatype_token = extract(tree, "old_datatype")
+    new_datatype_token = extract(tree, "new_datatype")
+
+    return NodeAnnotationChange(
+        id=id,
+        about_node=subject,
+        about_node_representation=s_representation,
+        annotation_property=predicate,
+        annotation_property_type=p_representation,
+        old_value=old,
+        new_value=new,
+        old_value_type=old_representation,
+        new_value_type=new_representation,
+        old_language=old_language_token,
+        new_language=new_language_token,
+        old_datatype=old_datatype_token,
+        new_datatype=new_datatype_token,
+    )
+
+
+def parse_change_relationship(tree, id):
+    subject_token = extract(tree, "entity_subject")
+    object_token = extract(tree, "entity_object")
+
+    subject, s_representation = get_entity_representation(subject_token)
+    object, o_representation = get_entity_representation(object_token)
+
+    old_token = extract(tree, "old_entity")
+    new_token = extract(tree, "new_entity")
+
+    old, old_representation = get_entity_representation(old_token)
+    new, new_representation = get_entity_representation(new_token)
+
+    edge = Edge(
+        subject=subject,
+        predicate=old,
+        object=object,
+        subject_representation=s_representation,
+        predicate_representation=old_representation,
+        object_representation=o_representation,
+    )
+
+    language_token = extract(tree, "language")
+    datatype_token = extract(tree, "datatype")
+
+    return PredicateChange(
+        id=id,
+        about_edge=edge,
+        old_value=old,
+        new_value=new,
+        old_value_type=old_representation,
+        new_value_type=new_representation,
+        language=language_token,
+        datatype=datatype_token,
+    )
+
+
+def parse_delete_edge(tree, id):
+    subject_token = extract(tree, "entity_subject")
+    predicate_token = extract(tree, "entity_predicate")
+    object_token = extract(tree, "entity_object_id")
+
+    subject, s_representation = get_entity_representation(subject_token)
+    predicate, p_representation = get_entity_representation(predicate_token)
+    object, o_representation = get_entity_representation(object_token)
+
+    if (
+        predicate == "rdfs:subClassOf"
+        or predicate == "<http://www.w3.org/2000/01/rdf-schema#subClassOf>"
+    ):
+        return RemoveUnder(
+            id=id,
+            subject=subject,
+            predicate=predicate,
+            object=object,
+            subject_type=s_representation,
+            predicate_type=p_representation,
+            object_type=o_representation,
+        )
+    else:
+        return EdgeDeletion(
+            id=id,
+            subject=subject,
+            predicate=predicate,
+            object=object,
+            subject_type=s_representation,
+            predicate_type=p_representation,
+            object_type=o_representation,
+        )
+
+
+def parse_create_edge(tree, id):
+    subject_token = extract(tree, "entity_subject")
+    predicate_token = extract(tree, "entity_predicate")
+    object_token = extract(tree, "entity_object_id")
+
+    subject, s_representation = get_entity_representation(subject_token)
+    predicate, p_representation = get_entity_representation(predicate_token)
+    object, o_representation = get_entity_representation(object_token)
+
+    if (
+        predicate == "rdfs:subClassOf"
+        or predicate == "<http://www.w3.org/2000/01/rdf-schema#subClassOf>"
+    ):
+        return PlaceUnder(
+            id=id,
+            subject=subject,
+            predicate=predicate,
+            object=object,
+            subject_type=s_representation,
+            predicate_type=p_representation,
+            object_type=o_representation,
+        )
+    else:
+        return EdgeCreation(
+            id=id,
+            subject=subject,
+            predicate=predicate,
+            object=object,
+            subject_type=s_representation,
+            predicate_type=p_representation,
+            object_type=o_representation,
+        )
+
+
+def parse_shallow(tree, id):
+    entity_token = extract(tree, "entity")
+    old_token = extract(tree, "old_entity")
+    new_token = extract(tree, "new_entity")
+
+    entity, e_representation = get_entity_representation(entity_token)
+    old_entity, o_representation = get_entity_representation(old_token)
+    new_entity, n_representation = get_entity_representation(new_token)
+
+    edge = Edge(
+        subject=entity,
+        object=old_entity,
+        subject_representation=e_representation,
+        object_representation=o_representation,
+    )
+
+    return NodeShallowing(
+        id=id,
+        about_edge=edge,
+        old_value=old_entity,
+        new_value=new_entity,
+        old_object_type=o_representation,
+        new_object_type=n_representation,
+    )
+
+
+def parse_deepen(tree, id):
+    entity_token = extract(tree, "entity")
+    old_token = extract(tree, "old_entity")
+    new_token = extract(tree, "new_entity")
+
+    entity, e_representation = get_entity_representation(entity_token)
+    old_entity, o_representation = get_entity_representation(old_token)
+    new_entity, n_representation = get_entity_representation(new_token)
+
+    edge = Edge(
+        subject=entity,
+        object=old_entity,
+        subject_representation=e_representation,
+        object_representation=o_representation,
+    )
+
+    return NodeDeepening(
+        id=id,
+        about_edge=edge,
+        old_value=old_entity,
+        new_value=new_entity,
+        old_object_type=o_representation,
+        new_object_type=n_representation,
+    )
+
+
+def parse_move(tree, id):
+    subject_token = extract(tree, "entity_subject")
+    predicate_token = extract(tree, "entity_predicate")
+    object_token = extract(tree, "entity_object")
+
+    subject, s_representation = get_entity_representation(subject_token)
+    predicate, p_representation = get_entity_representation(predicate_token)
+    object, o_representation = get_entity_representation(object_token)
+
+    old_token = extract(tree, "old_entity")
+    new_token = extract(tree, "new_entity")
+
+    old, old_representation = get_entity_representation(old_token)
+    new, new_representation = get_entity_representation(new_token)
+
+    edge = Edge(
+        subject=subject,
+        predicate=predicate,
+        object=object,
+        subject_representation=s_representation,
+        predicate_representation=p_representation,
+        object_representation=o_representation,
+    )
+
+    return NodeMove(
+        id=id,
+        about_edge=edge,
+        old_value=old,
+        new_value=new,
+        old_object_type=old_representation,
+        new_object_type=new_representation,
+    )
+
+
+def parse_delete(tree, id):
+    entity_token = extract(tree, "entity")
+    entity, representation = get_entity_representation(entity_token)
+
+    return NodeDeletion(
+        id=id, about_node=entity, about_node_representation=representation
+    )
+
+
+def parse_unobsolete(tree, id):
+    entity_token = extract(tree, "entity")
+    entity, representation = get_entity_representation(entity_token)
+
+    return NodeUnobsoletion(
+        id=id, about_node=entity, about_node_representation=representation
+    )
+
+
+def parse_obsolete(tree, id):
+    entity_token = extract(tree, "entity")
+    entity, representation = get_entity_representation(entity_token)
+
+    replacement_token = extract(tree, "replacement")
+
+    return NodeObsoletion(
+        id=id,
+        about_node=entity,
+        about_node_representation=representation,
+        has_direct_replacement=replacement_token,
+    )
+
+
+def parse_rename(tree, id):
+    old_token = extract(tree, "old_label")
+    new_token = extract(tree, "new_label")
+    old_language = extract(tree, "old_language")
+    new_language = extract(tree, "new_language")
+
+    # TODO old_token and new_token are enclosed in ''
+
+    term_id_token = extract(tree, "id")
+    if term_id_token is not None:
+        entity, representation = get_entity_representation(term_id_token)
+    else:
+        entity = None
+        representation = None
+
+    return NodeRename(
+        id=id,
+        about_node=entity,
+        about_node_representation=representation,
+        old_value=old_token,
+        new_value=new_token,
+        old_language=old_language,
+        new_language=new_language,
+    )
+
+
+def extract(tree, data):
+    node = get_next(tree.find_data(data))
+    if node is not None:
+        node_token = next(get_tokens(node))
+        return node_token
+    else:
+        return None
+
+
+def get_tokens(tree):
+    return tree.scan_values(lambda v: isinstance(v, Token))
+
+
+def get_next(generator):
+    try:
+        res = next(generator)
+        return res
+    except StopIteration:
+        return None
+
+
+def get_entity_representation(entity):
+    first_character = entity[0]
+    last_character = entity[-1:]
+    if first_character == "<" and last_character == ">":
+        return entity, "uri"  # not removing brackets (TODO why?)
+    if first_character == "'" and last_character == "'" and entity[1] != "'":
+        return entity[1:-1], "label"
+    if first_character == '"' and last_character == '"':
+        return entity[1:-1], "literal"
+    if entity[0:3] == '"""' and entity[-3:] == '"""':
+        return entity[3:-3], "literal"
+    if entity[0:3] == "'''" and entity[-3:] == "'''":
+        return entity[3:-3], "literal"
+
+    # TODO: use predefined set of prefixes to identify CURIEs
+    return entity, "curie"
+    # return entity, "error"
+
+
+if __name__ == "__main__":
+    #
+    # MANUAL TESTING
+    #
+    example1 = "renamea 'abnormal ear' to 'abnormal ear morphology'"
+    tree = kgcl_parser.parse(example1)
+    print(tree)
+    old = next(tree.find_data("old_label"))
+    print(old)
+    old_token = next(get_tokens(old))
+    print(old_token)
diff --git a/kgcl/kgcl.py b/kgcl/kgcl.py
new file mode 100644
index 00000000..ec6ad453
--- /dev/null
+++ b/kgcl/kgcl.py
@@ -0,0 +1,46 @@
+import click
+import kgcl.grammar.parser
+import kgcl.apply.graph_transformer
+import rdflib
+from rdflib.util import guess_format
+
+import sys
+
+sys.path.append("../")
+import python.kgcl
+
+
+class Config(object):
+    def __init__(self):
+        self.verbose = False
+
+
+pass_config = click.make_pass_decorator(Config, ensure=True)
+
+
+@click.command()
+@click.argument("graph", type=click.Path(), required=True)
+@click.argument("kgcl", type=click.File("r"), required=True)
+@click.argument("output", type=click.Path(), required=True)
+# @click.option("--verbose", "-v", is_flag=True, help="Print more output.")
+@pass_config
+def cli(config, graph, kgcl, output):
+
+    # read kgcl commands from file
+    kgcl_patch = kgcl.read()
+
+    # parser kgcl commands
+    parsed_patch = grammar.parser.parse(kgcl_patch)
+
+    # apply kgcl commands as SPARQL UPDATE queries to graph
+    g = rdflib.Graph()
+    g.load(graph, format=guess_format(graph))
+    # g.parse(graph)  # , format="nt") #TODO: this doesn't always work
+    apply.graph_transformer.apply_patch(parsed_patch, g)
+
+    # save updated graph
+    g.serialize(destination=output, format="nt")
+
+
+if __name__ == "__main__":
+    cli()
diff --git a/kgcl/kgcl_diff.py b/kgcl/kgcl_diff.py
new file mode 100644
index 00000000..8877657f
--- /dev/null
+++ b/kgcl/kgcl_diff.py
@@ -0,0 +1,192 @@
+import click
+import os
+import grammar.parser
+import apply.graph_transformer
+import rdflib
+from datetime import datetime
+from pretty_print_kgcl import render_instances
+
+import diff.diff_2_kgcl_single as single
+from diff.diff_2_kgcl_single import SingleTripleChangeSummary
+import diff.diff_2_kgcl_existential as existential
+from diff.diff_2_kgcl_existential import ExistentialChangeSummary
+import diff.diff_2_kgcl_triple_annotation as annotation
+from diff.diff_2_kgcl_triple_annotation import TripleAnnotationChangeSummary
+
+import sys
+
+
+class Config(object):
+    def __init__(self):
+        self.verbose = False
+
+
+pass_config = click.make_pass_decorator(Config, ensure=True)
+
+
+def ts():
+    now = datetime.now()
+    dt_string = now.strftime("%H:%M:%S ")
+    return dt_string
+
+
+@click.command()
+@click.argument("ingraph", type=click.Path(), required=True)
+@click.argument("outgraph", type=click.Path(), required=True)
+@click.argument("output", type=click.Path(), required=True)
+# @click.option("--verbose", "-v", is_flag=True, help="Print more output.")
+@pass_config
+def cli(config, ingraph, outgraph, output):
+
+    os.mkdir(output)
+
+    # load graphs
+    g1 = rdflib.Graph()
+    g2 = rdflib.Graph()
+    # g1.load(ingraph, format="nt")
+    g1.parse(ingraph)
+    print(ts() + "Loaded Graph 1")
+    g2.parse(outgraph)
+    # g2.load(outgraph, format="nt")
+    print(ts() + "Loaded Graph 2")
+
+    # compute diff
+    existential_summary = existential.generate_atomic_existential_commands(g1, g2)
+    print(ts() + "Generated Diff for Existentials")
+    triple_annotation_summary = annotation.generate_triple_annotation_commands(g1, g2)
+    print(ts() + "Generated Diff for Triple Annotations")
+    single_triple_summary = single.generate_thin_triple_commands(g1, g2)
+    print(ts() + "Generated Diff for Thin Triples")
+
+    # write summary report
+    with open(output + "/kgcl_summary.txt", "w") as f:
+        f.write(existential_summary.get_summary_KGCL_commands())
+        f.write(triple_annotation_summary.get_summary_KGCL_commands())
+        f.write(single_triple_summary.get_summary_KGCL_commands())
+
+    # write non-deterministic diff report
+    nd_node_moves = single_triple_summary.get_non_deterministic_node_moves()
+    nd_predicate_changes = (
+        single_triple_summary.get_non_deterministic_predicate_changes()
+    )
+    nd_renamings = single_triple_summary.get_non_deterministic_renamings()
+
+    non_deterministic_folder = os.path.join(output, "non_deterministic")
+    os.mkdir(non_deterministic_folder)
+    with open(non_deterministic_folder + "/node_moves.txt", "w") as f:
+        f.write(render_non_deterministic_diff(nd_node_moves))
+    with open(non_deterministic_folder + "/predicate_changes.txt", "w") as f:
+        f.write(render_non_deterministic_diff(nd_predicate_changes))
+    with open(non_deterministic_folder + "/renamings.txt", "w") as f:
+        f.write(render_non_deterministic_diff(nd_renamings))
+
+    # get KGCL commands
+    kgcl_commands = existential_summary.get_commands()
+    kgcl_commands += triple_annotation_summary.get_commands()
+    kgcl_commands += single_triple_summary.get_commands()
+
+    # write KGCL commands
+    with open(output + "/patch.kgcl", "w") as f:
+        for k in kgcl_commands:
+            f.write(k)
+            f.write("\n")
+
+    # write distinct KGCL commands
+    patch_folder = os.path.join(output, "patch")
+    os.mkdir(patch_folder)
+
+    with open(patch_folder + "/existential_additions.txt", "w") as f:
+        for k in existential_summary.get_existential_additions():
+            f.write(k)
+            f.write("\n")
+
+    with open(patch_folder + "/existential_deletions.txt", "w") as f:
+        for k in existential_summary.get_existential_deletions():
+            f.write(k)
+            f.write("\n")
+
+    with open(patch_folder + "/triple_annotation_additions.txt", "w") as f:
+        for k in triple_annotation_summary.get_triple_annotation_additions():
+            f.write(k)
+            f.write("\n")
+
+    with open(patch_folder + "/triple_annotation_deletions.txt", "w") as f:
+        for k in triple_annotation_summary.get_triple_annotation_deletions():
+            f.write(k)
+            f.write("\n")
+
+    with open(patch_folder + "/renamings.txt", "w") as f:
+        for k in single_triple_summary.get_renamings():
+            f.write(k)
+            f.write("\n")
+
+    with open(patch_folder + "/class_creations.txt", "w") as f:
+        for k in single_triple_summary.get_class_creations():
+            f.write(k)
+            f.write("\n")
+
+    with open(patch_folder + "/subsumption_creations.txt", "w") as f:
+        for k in single_triple_summary.get_subsumption_creations():
+            f.write(k)
+            f.write("\n")
+
+    with open(patch_folder + "/subsumption_deletions.txt", "w") as f:
+        for k in single_triple_summary.get_subsumption_deletions():
+            f.write(k)
+            f.write("\n")
+
+    with open(patch_folder + "/predicate_changes.txt", "w") as f:
+        for k in single_triple_summary.get_predicate_changes():
+            f.write(k)
+            f.write("\n")
+
+    with open(patch_folder + "/node_moves.txt", "w") as f:
+        for k in single_triple_summary.get_node_moves():
+            f.write(k)
+            f.write("\n")
+
+    with open(patch_folder + "/synonym_creations.txt", "w") as f:
+        for k in single_triple_summary.get_synonym_creations():
+            f.write(k)
+            f.write("\n")
+
+    with open(patch_folder + "/edge_creations.txt", "w") as f:
+        for k in single_triple_summary.get_edge_creations():
+            f.write(k)
+            f.write("\n")
+
+    with open(patch_folder + "/edge_deletions.txt", "w") as f:
+        for k in single_triple_summary.get_edge_deletions():
+            f.write(k)
+            f.write("\n")
+
+    pp_kgcl_commands = render_instances(kgcl_commands, g1)
+    with open(output + "/pp_patch.kgcl", "w") as f:
+        for a in pp_kgcl_commands:
+            f.write(a)
+            f.write("\n")
+
+
+# a non-deterministic diff consists of a list of tuples.
+# A tuple contains two sets of triples that are involved in the non-deterministic diff
+def render_non_deterministic_diff(nd):
+    out = ""
+    for tuple in nd:
+        from_triples = tuple[0]
+        to_triples = tuple[1]
+        rendering = ""
+        for s, p, o in from_triples:
+            triple = " (" + str(s) + "," + str(p) + "," + str(o) + ")\n"
+            rendering += triple
+        rendering += "->\n"
+        for s, p, o in to_triples:
+            triple = " (" + str(s) + "," + str(p) + "," + str(o) + ")\n"
+            rendering += triple
+
+        out += rendering + "\n\n"
+
+    return out
+
+
+if __name__ == "__main__":
+    cli()
diff --git a/kgcl/model/__init__.py b/kgcl/model/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/kgcl/model/kgcl.py b/kgcl/model/kgcl.py
index b231a08c..bb0f51c2 100644
--- a/kgcl/model/kgcl.py
+++ b/kgcl/model/kgcl.py
@@ -1,5 +1,5 @@
 # Auto generated from kgcl.yaml by pythongen.py version: 0.9.0
-# Generation date: 2021-08-19 15:44
+# Generation date: 2021-10-13 18:55
 # Schema: kgcl
 #
 # id: https://w3id.org/kgcl
@@ -93,6 +93,10 @@ class EdgeDeletionId(EdgeChangeId):
     pass
 
 
+class RemoveUnderId(EdgeDeletionId):
+    pass
+
+
 class EdgeObsoletionId(EdgeChangeId):
     pass
 
@@ -295,6 +299,12 @@ class SimpleChange(Change):
     id: Union[str, SimpleChangeId] = None
     old_value: Optional[str] = None
     new_value: Optional[str] = None
+    old_value_type: Optional[str] = None
+    new_value_type: Optional[str] = None
+    new_language: Optional[str] = None
+    old_language: Optional[str] = None
+    new_datatype: Optional[str] = None
+    old_datatype: Optional[str] = None
 
     def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
         if self.old_value is not None and not isinstance(self.old_value, str):
@@ -303,6 +313,24 @@ def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
         if self.new_value is not None and not isinstance(self.new_value, str):
             self.new_value = str(self.new_value)
 
+        if self.old_value_type is not None and not isinstance(self.old_value_type, str):
+            self.old_value_type = str(self.old_value_type)
+
+        if self.new_value_type is not None and not isinstance(self.new_value_type, str):
+            self.new_value_type = str(self.new_value_type)
+
+        if self.new_language is not None and not isinstance(self.new_language, str):
+            self.new_language = str(self.new_language)
+
+        if self.old_language is not None and not isinstance(self.old_language, str):
+            self.old_language = str(self.old_language)
+
+        if self.new_datatype is not None and not isinstance(self.new_datatype, str):
+            self.new_datatype = str(self.new_datatype)
+
+        if self.old_datatype is not None and not isinstance(self.old_datatype, str):
+            self.old_datatype = str(self.old_datatype)
+
         super().__post_init__(**kwargs)
 
 
@@ -485,6 +513,7 @@ def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
         if self.new_value is not None and not isinstance(self.new_value, str):
             self.new_value = str(self.new_value)
 
+
         super().__post_init__(**kwargs)
 
 
@@ -645,12 +674,24 @@ class EdgeChange(SimpleChange):
 
     id: Union[str, EdgeChangeId] = None
     about_edge: Optional[Union[dict, Edge]] = None
+    object_type: Optional[str] = None
+    language: Optional[str] = None
+    datatype: Optional[str] = None
     subject: Optional[Union[str, NodeId]] = None
 
     def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
         if self.about_edge is not None and not isinstance(self.about_edge, Edge):
             self.about_edge = Edge(**as_dict(self.about_edge))
 
+        if self.object_type is not None and not isinstance(self.object_type, str):
+            self.object_type = str(self.object_type)
+
+        if self.language is not None and not isinstance(self.language, str):
+            self.language = str(self.language)
+
+        if self.datatype is not None and not isinstance(self.datatype, str):
+            self.datatype = str(self.datatype)
+
         if self.subject is not None and not isinstance(self.subject, NodeId):
             self.subject = NodeId(self.subject)
 
@@ -673,6 +714,9 @@ class EdgeCreation(EdgeChange):
     subject: Optional[Union[str, NodeId]] = None
     predicate: Optional[Union[str, NodeId]] = None
     object: Optional[Union[str, NodeId]] = None
+    subject_type: Optional[str] = None
+    predicate_type: Optional[str] = None
+    object_type: Optional[str] = None
     annotation_set: Optional[Union[dict, Annotation]] = None
     change_description: Optional[str] = None
 
@@ -691,6 +735,15 @@ def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
         if self.object is not None and not isinstance(self.object, NodeId):
             self.object = NodeId(self.object)
 
+        if self.subject_type is not None and not isinstance(self.subject_type, str):
+            self.subject_type = str(self.subject_type)
+
+        if self.predicate_type is not None and not isinstance(self.predicate_type, str):
+            self.predicate_type = str(self.predicate_type)
+
+        if self.object_type is not None and not isinstance(self.object_type, str):
+            self.object_type = str(self.object_type)
+
         if self.annotation_set is not None and not isinstance(self.annotation_set, Annotation):
             self.annotation_set = Annotation(**as_dict(self.annotation_set))
 
@@ -739,6 +792,9 @@ class EdgeDeletion(EdgeChange):
     subject: Optional[Union[str, NodeId]] = None
     predicate: Optional[Union[str, NodeId]] = None
     object: Optional[Union[str, NodeId]] = None
+    subject_type: Optional[str] = None
+    predicate_type: Optional[str] = None
+    object_type: Optional[str] = None
     annotation_set: Optional[Union[dict, Annotation]] = None
     change_description: Optional[str] = None
 
@@ -757,6 +813,15 @@ def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
         if self.object is not None and not isinstance(self.object, NodeId):
             self.object = NodeId(self.object)
 
+        if self.subject_type is not None and not isinstance(self.subject_type, str):
+            self.subject_type = str(self.subject_type)
+
+        if self.predicate_type is not None and not isinstance(self.predicate_type, str):
+            self.predicate_type = str(self.predicate_type)
+
+        if self.object_type is not None and not isinstance(self.object_type, str):
+            self.object_type = str(self.object_type)
+
         if self.annotation_set is not None and not isinstance(self.annotation_set, Annotation):
             self.annotation_set = Annotation(**as_dict(self.annotation_set))
 
@@ -766,6 +831,29 @@ def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
         super().__post_init__(**kwargs)
 
 
+@dataclass
+class RemoveUnder(EdgeDeletion):
+    """
+    An edge deletion where the predicate is owl:subClassOf
+    """
+    _inherited_slots: ClassVar[List[str]] = []
+
+    class_class_uri: ClassVar[URIRef] = KGCL.RemoveUnder
+    class_class_curie: ClassVar[str] = "kgcl:RemoveUnder"
+    class_name: ClassVar[str] = "remove under"
+    class_model_uri: ClassVar[URIRef] = KGCL.RemoveUnder
+
+    id: Union[str, RemoveUnderId] = None
+
+    def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
+        if self._is_empty(self.id):
+            self.MissingRequiredField("id")
+        if not isinstance(self.id, RemoveUnderId):
+            self.id = RemoveUnderId(self.id)
+
+        super().__post_init__(**kwargs)
+
+
 @dataclass
 class EdgeObsoletion(EdgeChange):
     """
@@ -893,6 +981,8 @@ class NodeMove(EdgeChange):
     class_model_uri: ClassVar[URIRef] = KGCL.NodeMove
 
     id: Union[str, NodeMoveId] = None
+    old_object_type: Optional[str] = None
+    new_object_type: Optional[str] = None
     change_description: Optional[str] = None
 
     def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
@@ -901,6 +991,12 @@ def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
         if not isinstance(self.id, NodeMoveId):
             self.id = NodeMoveId(self.id)
 
+        if self.old_object_type is not None and not isinstance(self.old_object_type, str):
+            self.old_object_type = str(self.old_object_type)
+
+        if self.new_object_type is not None and not isinstance(self.new_object_type, str):
+            self.new_object_type = str(self.new_object_type)
+
         if self.change_description is not None and not isinstance(self.change_description, str):
             self.change_description = str(self.change_description)
 
@@ -1049,12 +1145,15 @@ class NodeChange(SimpleChange):
 
     id: Union[str, NodeChangeId] = None
     about_node: Optional[Union[str, NodeId]] = None
+    about_node_representation: Optional[str] = None
     language: Optional[str] = None
 
     def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
         if self.about_node is not None and not isinstance(self.about_node, NodeId):
             self.about_node = NodeId(self.about_node)
 
+        if self.about_node_representation is not None and not isinstance(self.about_node_representation, str):
+            self.about_node_representation = str(self.about_node_representation)
         if self.language is not None and not isinstance(self.language, str):
             self.language = str(self.language)
 
@@ -1077,6 +1176,8 @@ class NodeRename(NodeChange):
     old_value: Optional[str] = None
     new_value: Optional[str] = None
     has_textual_diff: Optional[Union[dict, "TextualDiff"]] = None
+    new_language: Optional[str] = None
+    old_language: Optional[str] = None
     change_description: Optional[str] = None
 
     def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
@@ -1094,6 +1195,12 @@ def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
         if self.has_textual_diff is not None and not isinstance(self.has_textual_diff, TextualDiff):
             self.has_textual_diff = TextualDiff()
 
+        if self.new_language is not None and not isinstance(self.new_language, str):
+            self.new_language = str(self.new_language)
+
+        if self.old_language is not None and not isinstance(self.old_language, str):
+            self.old_language = str(self.old_language)
+
         if self.change_description is not None and not isinstance(self.change_description, str):
             self.change_description = str(self.change_description)
 
@@ -1148,6 +1255,8 @@ class NodeAnnotationChange(NodeChange):
     class_model_uri: ClassVar[URIRef] = KGCL.NodeAnnotationChange
 
     id: Union[str, NodeAnnotationChangeId] = None
+    annotation_property: Optional[str] = None
+    annotation_property_type: Optional[str] = None
 
     def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
         if self._is_empty(self.id):
@@ -1155,6 +1264,12 @@ def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
         if not isinstance(self.id, NodeAnnotationChangeId):
             self.id = NodeAnnotationChangeId(self.id)
 
+        if self.annotation_property is not None and not isinstance(self.annotation_property, str):
+            self.annotation_property = str(self.annotation_property)
+
+        if self.annotation_property_type is not None and not isinstance(self.annotation_property_type, str):
+            self.annotation_property_type = str(self.annotation_property_type)
+
         super().__post_init__(**kwargs)
 
 
@@ -1215,6 +1330,8 @@ class NewSynonym(NodeSynonymChange):
 
     id: Union[str, NewSynonymId] = None
     new_value: Optional[str] = None
+    language: Optional[str] = None
+    qualifier: Optional[str] = None
 
     def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
         if self._is_empty(self.id):
@@ -1225,6 +1342,12 @@ def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
         if self.new_value is not None and not isinstance(self.new_value, str):
             self.new_value = str(self.new_value)
 
+        if self.language is not None and not isinstance(self.language, str):
+            self.language = str(self.language)
+
+        if self.qualifier is not None and not isinstance(self.qualifier, str):
+            self.qualifier = str(self.qualifier)
+
         super().__post_init__(**kwargs)
 
 
@@ -1627,6 +1750,7 @@ class NodeCreation(NodeChange):
     name: Optional[str] = None
     owl_type: Optional[Union[str, "OwlTypeEnum"]] = None
     annotation_set: Optional[Union[dict, Annotation]] = None
+    language: Optional[str] = None
     change_description: Optional[str] = None
 
     def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
@@ -1647,6 +1771,9 @@ def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
         if self.annotation_set is not None and not isinstance(self.annotation_set, Annotation):
             self.annotation_set = Annotation(**as_dict(self.annotation_set))
 
+        if self.language is not None and not isinstance(self.language, str):
+            self.language = str(self.language)
+
         if self.change_description is not None and not isinstance(self.change_description, str):
             self.change_description = str(self.change_description)
 
@@ -1945,6 +2072,9 @@ class slots:
 slots.about_edge = Slot(uri=KGCL.about_edge, name="about edge", curie=KGCL.curie('about_edge'),
                    model_uri=KGCL.about_edge, domain=None, range=Optional[Union[dict, Edge]])
 
+slots.about_node_representation = Slot(uri=KGCL.about_node_representation, name="about node representation", curie=KGCL.curie('about_node_representation'),
+                   model_uri=KGCL.about_node_representation, domain=None, range=Optional[str])
+
 slots.target = Slot(uri=KGCL.target, name="target", curie=KGCL.curie('target'),
                    model_uri=KGCL.target, domain=None, range=Optional[str])
 
@@ -1954,9 +2084,75 @@ class slots:
 slots.new_value = Slot(uri=KGCL.new_value, name="new value", curie=KGCL.curie('new_value'),
                    model_uri=KGCL.new_value, domain=None, range=Optional[str])
 
+slots.language = Slot(uri=KGCL.language, name="language", curie=KGCL.curie('language'),
+                   model_uri=KGCL.language, domain=None, range=Optional[str])
+
+slots.datatype = Slot(uri=KGCL.datatype, name="datatype", curie=KGCL.curie('datatype'),
+                   model_uri=KGCL.datatype, domain=None, range=Optional[str])
+
+slots.new_datatype = Slot(uri=KGCL.new_datatype, name="new datatype", curie=KGCL.curie('new_datatype'),
+                   model_uri=KGCL.new_datatype, domain=None, range=Optional[str])
+
+slots.old_datatype = Slot(uri=KGCL.old_datatype, name="old datatype", curie=KGCL.curie('old_datatype'),
+                   model_uri=KGCL.old_datatype, domain=None, range=Optional[str])
+
+slots.new_language = Slot(uri=KGCL.new_language, name="new language", curie=KGCL.curie('new_language'),
+                   model_uri=KGCL.new_language, domain=None, range=Optional[str])
+
+slots.old_language = Slot(uri=KGCL.old_language, name="old language", curie=KGCL.curie('old_language'),
+                   model_uri=KGCL.old_language, domain=None, range=Optional[str])
+
+slots.qualifier = Slot(uri=KGCL.qualifier, name="qualifier", curie=KGCL.curie('qualifier'),
+                   model_uri=KGCL.qualifier, domain=None, range=Optional[str])
+
+slots.subclass = Slot(uri=KGCL.subclass, name="subclass", curie=KGCL.curie('subclass'),
+                   model_uri=KGCL.subclass, domain=None, range=Optional[str])
+
+slots.new_subclass = Slot(uri=KGCL.new_subclass, name="new subclass", curie=KGCL.curie('new_subclass'),
+                   model_uri=KGCL.new_subclass, domain=None, range=Optional[str])
+
+slots.new_property = Slot(uri=KGCL.new_property, name="new property", curie=KGCL.curie('new_property'),
+                   model_uri=KGCL.new_property, domain=None, range=Optional[str])
+
+slots.new_filler = Slot(uri=KGCL.new_filler, name="new filler", curie=KGCL.curie('new_filler'),
+                   model_uri=KGCL.new_filler, domain=None, range=Optional[str])
+
+slots.object_type = Slot(uri=KGCL.object_type, name="object type", curie=KGCL.curie('object_type'),
+                   model_uri=KGCL.object_type, domain=None, range=Optional[str])
+
+slots.new_object_type = Slot(uri=KGCL.new_object_type, name="new object type", curie=KGCL.curie('new_object_type'),
+                   model_uri=KGCL.new_object_type, domain=None, range=Optional[str])
+
+slots.old_object_type = Slot(uri=KGCL.old_object_type, name="old object type", curie=KGCL.curie('old_object_type'),
+                   model_uri=KGCL.old_object_type, domain=None, range=Optional[str])
+
+slots.new_value_type = Slot(uri=KGCL.new_value_type, name="new value type", curie=KGCL.curie('new_value_type'),
+                   model_uri=KGCL.new_value_type, domain=None, range=Optional[str])
+
+slots.old_value_type = Slot(uri=KGCL.old_value_type, name="old value type", curie=KGCL.curie('old_value_type'),
+                   model_uri=KGCL.old_value_type, domain=None, range=Optional[str])
+
+slots.subject_type = Slot(uri=KGCL.subject_type, name="subject type", curie=KGCL.curie('subject_type'),
+                   model_uri=KGCL.subject_type, domain=None, range=Optional[str])
+
+slots.subclass_type = Slot(uri=KGCL.subclass_type, name="subclass type", curie=KGCL.curie('subclass_type'),
+                   model_uri=KGCL.subclass_type, domain=None, range=Optional[str])
+
+slots.superclass_type = Slot(uri=KGCL.superclass_type, name="superclass type", curie=KGCL.curie('superclass_type'),
+                   model_uri=KGCL.superclass_type, domain=None, range=Optional[str])
+
+slots.predicate_type = Slot(uri=KGCL.predicate_type, name="predicate type", curie=KGCL.curie('predicate_type'),
+                   model_uri=KGCL.predicate_type, domain=None, range=Optional[str])
+
 slots.in_subset = Slot(uri=KGCL.in_subset, name="in subset", curie=KGCL.curie('in_subset'),
                    model_uri=KGCL.in_subset, domain=None, range=Optional[Union[dict, OntologySubset]])
 
+slots.annotation_property = Slot(uri=KGCL.annotation_property, name="annotation property", curie=KGCL.curie('annotation_property'),
+                   model_uri=KGCL.annotation_property, domain=None, range=Optional[str])
+
+slots.annotation_property_type = Slot(uri=KGCL.annotation_property_type, name="annotation property type", curie=KGCL.curie('annotation_property_type'),
+                   model_uri=KGCL.annotation_property_type, domain=None, range=Optional[str])
+
 slots.change_description = Slot(uri=KGCL.change_description, name="change description", curie=KGCL.curie('change_description'),
                    model_uri=KGCL.change_description, domain=None, range=Optional[str])
 
diff --git a/kgcl/model/ontology_model.py b/kgcl/model/ontology_model.py
index 1fa5f6b2..35716a3d 100644
--- a/kgcl/model/ontology_model.py
+++ b/kgcl/model/ontology_model.py
@@ -1,5 +1,5 @@
 # Auto generated from ontology_model.yaml by pythongen.py version: 0.9.0
-# Generation date: 2021-06-25 07:49
+# Generation date: 2021-09-23 19:44
 # Schema: kgcl-ontology-model
 #
 # id: https://w3id.org/kgcl/ontology
@@ -10,7 +10,7 @@
 import dataclasses
 import sys
 import re
-from jsonasobj2 import JsonObj
+from jsonasobj2 import JsonObj, as_dict
 from typing import Optional, List, Union, Dict, ClassVar, Any
 from dataclasses import dataclass
 from linkml_runtime.linkml_model.meta import EnumDefinition, PermissibleValue, PvFormulaOptions
@@ -107,6 +107,8 @@ class Annotation(PropertyValue):
     property: Optional[Union[str, NodeId]] = None
     filler: Optional[str] = None
     annotation_set: Optional[Union[dict, "Annotation"]] = None
+    property_type: Optional[str] = None
+    filler_type: Optional[str] = None
 
     def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
         if self.property is not None and not isinstance(self.property, NodeId):
@@ -116,7 +118,13 @@ def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
             self.filler = str(self.filler)
 
         if self.annotation_set is not None and not isinstance(self.annotation_set, Annotation):
-            self.annotation_set = Annotation(**self.annotation_set)
+            self.annotation_set = Annotation(**as_dict(self.annotation_set))
+
+        if self.property_type is not None and not isinstance(self.property_type, str):
+            self.property_type = str(self.property_type)
+
+        if self.filler_type is not None and not isinstance(self.filler_type, str):
+            self.filler_type = str(self.filler_type)
 
         super().__post_init__(**kwargs)
 
@@ -148,7 +156,7 @@ def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
             self.name = str(self.name)
 
         if self.annotation_set is not None and not isinstance(self.annotation_set, Annotation):
-            self.annotation_set = Annotation(**self.annotation_set)
+            self.annotation_set = Annotation(**as_dict(self.annotation_set))
 
         if self.owl_type is not None and not isinstance(self.owl_type, OwlTypeEnum):
             self.owl_type = OwlTypeEnum(self.owl_type)
@@ -217,6 +225,9 @@ class Edge(OntologyElement):
     subject: Optional[Union[str, NodeId]] = None
     predicate: Optional[Union[str, NodeId]] = None
     object: Optional[Union[str, NodeId]] = None
+    subject_representation: Optional[str] = None
+    predicate_representation: Optional[str] = None
+    object_representation: Optional[str] = None
     annotation_set: Optional[Union[dict, Annotation]] = None
 
     def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
@@ -229,8 +240,17 @@ def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
         if self.object is not None and not isinstance(self.object, NodeId):
             self.object = NodeId(self.object)
 
+        if self.subject_representation is not None and not isinstance(self.subject_representation, str):
+            self.subject_representation = str(self.subject_representation)
+
+        if self.predicate_representation is not None and not isinstance(self.predicate_representation, str):
+            self.predicate_representation = str(self.predicate_representation)
+
+        if self.object_representation is not None and not isinstance(self.object_representation, str):
+            self.object_representation = str(self.object_representation)
+
         if self.annotation_set is not None and not isinstance(self.annotation_set, Annotation):
-            self.annotation_set = Annotation(**self.annotation_set)
+            self.annotation_set = Annotation(**as_dict(self.annotation_set))
 
         super().__post_init__(**kwargs)
 
@@ -310,5 +330,20 @@ class slots:
 slots.filler = Slot(uri=OM.filler, name="filler", curie=OM.curie('filler'),
                    model_uri=OM.filler, domain=None, range=Optional[str])
 
+slots.property_type = Slot(uri=OM.property_type, name="property type", curie=OM.curie('property_type'),
+                   model_uri=OM.property_type, domain=None, range=Optional[str])
+
+slots.filler_type = Slot(uri=OM.filler_type, name="filler type", curie=OM.curie('filler_type'),
+                   model_uri=OM.filler_type, domain=None, range=Optional[str])
+
+slots.subject_representation = Slot(uri=OM.subject_representation, name="subject representation", curie=OM.curie('subject_representation'),
+                   model_uri=OM.subject_representation, domain=None, range=Optional[str])
+
+slots.predicate_representation = Slot(uri=OM.predicate_representation, name="predicate representation", curie=OM.curie('predicate_representation'),
+                   model_uri=OM.predicate_representation, domain=None, range=Optional[str])
+
+slots.object_representation = Slot(uri=OM.object_representation, name="object representation", curie=OM.curie('object_representation'),
+                   model_uri=OM.object_representation, domain=None, range=Optional[str])
+
 slots.property_value_set = Slot(uri=OM.property_value_set, name="property value set", curie=OM.curie('property_value_set'),
                    model_uri=OM.property_value_set, domain=None, range=Optional[Union[Union[dict, PropertyValue], List[Union[dict, PropertyValue]]]])
diff --git a/parser/app.py b/kgcl/render_kgcl.py
similarity index 61%
rename from parser/app.py
rename to kgcl/render_kgcl.py
index f63187b4..4ba75a66 100644
--- a/parser/app.py
+++ b/kgcl/render_kgcl.py
@@ -1,9 +1,4 @@
-from flask import (
-    Flask,
-    render_template,
-    request,
-)
-from kgcl import (
+from kgcl.model.kgcl import (
     NodeRename,
     NodeObsoletion,
     NodeUnobsoletion,
@@ -19,141 +14,6 @@
     NewSynonym,
     RemovedNodeFromSubset,
 )
-import parser
-import graph_transformer
-import rdflib
-
-app = Flask(__name__)
-
-
-# we could store changes in a data base - do we want to
-@app.route("/", methods=["POST", "GET"])
-def index():
-    # TODO: get examples from folder structure
-    examples = [
-        "rename",
-        "nodeCreation",
-        "nodeDeletionByLabel",
-        "nodeDeletionById",
-        "obsoleteByLabel",
-        "obsoleteById",
-        "unobsoleteById",
-        "nodeDeepening",
-        "nodeShallowing",
-        "move",
-        "edgeCreation",
-        "edgeDeletion",
-    ]
-
-    if request.method == "POST":
-
-        if "apply_changes" in request.form:
-
-            # get input graph from form
-            graphInput = request.form["graph"]
-
-            # get input kgcl statements from form
-            kgcl = request.form["kgcl"]
-
-            # TODO:
-            # 1. load triples without writing to a file first
-            # 2. handle kgcl input without writing to a file
-
-            # store kgcl statements
-            f = open("testData/kgcl", "w")
-            f.write(kgcl)
-            f.close()
-
-            # parse KGCL input
-            parsed_statements = parse(kgcl)
-
-            # store graph as file
-            f = open("testData/graph.nt", "w")
-            f.write(graphInput)
-            f.close()
-
-            # load input graph from file
-            g = rdflib.Graph()
-            g.load("testData/graph.nt", format="nt")
-
-            # transform graph
-            graph_transformer.transform_graph(parsed_statements, g)
-
-            # save graph
-            # TODO: get text representation of graph without writing to a file
-            g.serialize(destination="testData/transformation.nt", format="nt")
-
-        if "load_example" in request.form:
-            select = request.form.get("comp_select")
-            example = str(select)
-
-            f = open("testData/example/" + example + "/graph.nt", "r")
-            graph = f.read()
-            f.close()
-
-            f = open("testData/graph.nt", "w")
-            f.write(graph)
-            f.close()
-
-            f = open("testData/example/" + example + "/kgcl", "r")
-            kgcl = f.read()
-            f.close()
-
-            f = open("testData/kgcl", "w")
-            f.write(kgcl)
-            f.close()
-
-            # parse KGCL input
-            parsed_statements = parse(kgcl)
-
-            # load input graph from file
-            g = rdflib.Graph()
-            g.load("testData/graph.nt", format="nt")
-
-            # transform graph
-            graph_transformer.transform_graph(parsed_statements, g)
-
-            # save graph
-            g.serialize(destination="testData/transformation.nt", format="nt")
-
-        # load graph
-        f = open("testData/graph.nt", "r")
-        graph = f.read()
-        f.close()
-
-        # load kgcl
-        f = open("testData/kgcl", "r")
-        kgcl = f.read()
-        f.close()
-
-        # parse KGCL input
-        output = parse(kgcl)
-
-        # prepare parsed
-        output_rendering = ""
-        for o in output:
-            output_rendering += render(o) + "\n"
-
-        # load transformed graph
-        f = open("testData/transformation.nt", "r")
-        transformation = f.read()
-        f.close()
-
-        return render_template(
-            "index.html",
-            inputGraph=graph,
-            inputKGCL=kgcl,
-            parsedKGCL=output_rendering,
-            outputGraph=transformation,
-            examples=examples,
-        )
-
-    else:
-        return render_template("index.html", examples=examples)
-
-
-def parse(input):
-    return parser.parse(input)
 
 
 def render(kgclInstance):
@@ -384,7 +244,3 @@ def render(kgclInstance):
         )
 
     return render
-
-
-if __name__ == "__main__":
-    app.run(debug=True)
diff --git a/kgcl/requirements.txt b/kgcl/requirements.txt
new file mode 100644
index 00000000..7559b0cf
--- /dev/null
+++ b/kgcl/requirements.txt
@@ -0,0 +1,4 @@
+linkml >= 1.0.1
+mkdocs
+Flask
+lark 
diff --git a/kgcl/setup.py b/kgcl/setup.py
new file mode 100644
index 00000000..1926648e
--- /dev/null
+++ b/kgcl/setup.py
@@ -0,0 +1,11 @@
+from setuptools import setup
+
+setup(
+    name="kgcl",  # this is what we pip install
+    version="0.0.1",
+    description="Knowledge Graph Change Language",
+    py_modules=[
+        "kgcl"
+    ],  # list of python code modules - this is the code I want to distribute (this is what people import - not what they pip-install)
+    package_dir={"": "kgcl"},
+),
diff --git a/parser/examples b/parser/examples
deleted file mode 100644
index afd2c5b0..00000000
--- a/parser/examples
+++ /dev/null
@@ -1,31 +0,0 @@
-###
-### EXAMPLE collection
-###
-
-#example2 = "rename UBERON:0002398 from 'manus' to 'hand'"
-#UBERON would be encoded in the field 'about'
-
-#mondo template:
-# name, about, title, labels, assignees (TODO: what is this table?)
-#an issue is made by specifying the following:
-#1. Mondo term (ID and Label): 
-#2. Suggested new label: 
-#3. Your nano-attribution (ORCID) If you don't have an ORCID, you can sign up for one here
-#4. Optional: Any additional information (like supporting evidence, PubMed ID, etc.)
-#
-#so, 1. corresponds to old_value,
-#    2. corresponds to new_value,
-#    3. corresponds to creator or contributor(?),
-#    4. corresponds to change_description
- 
-#examples for OBSOLETE 
-#Obsolete bad with replacement new
-#Obsolete foo
-#Obsolete foo and add relplaced_by bar (TODO: ask about connective 'and')
-
-#examples for MOVE 
-
-#Move bar from under baz to under fred
-#move ‘abnormal hindlimb’ -> ‘abnormal limb’
-#Move ‘abnormal hindlimb’ from under ‘abnormal ear’ -> under ‘abnormal limb’
-#Move finger from hand -> part of some hand
diff --git a/parser/graph_transformer.py b/parser/graph_transformer.py
deleted file mode 100644
index 922b213e..00000000
--- a/parser/graph_transformer.py
+++ /dev/null
@@ -1,35 +0,0 @@
-import kgcl_2_sparql
-import rdflib
-
-
-# used for local testing
-def transform(kgclInstance):
-
-    # test with obi_core
-    g = rdflib.Graph()
-    g.load("testData/obi_core.nt", format="nt")
-
-    query = kgcl_2_sparql.convert(kgclInstance)
-    g.update(query)
-    g.serialize(destination="testData/output.nt", format="nt")
-
-
-# used for local testing
-def transform_set(kgclInstance):
-
-    # test with obi_core
-    g = rdflib.Graph()
-    g.load("testData/obi_core.nt", format="nt")
-
-    for i in kgclInstance:
-        query = kgcl_2_sparql.convert(i)
-        g.update(query)
-
-    g.serialize(destination="testData/output.nt", format="nt")
-
-
-def transform_graph(kgclInstance, graph):
-
-    for i in kgclInstance:
-        query = kgcl_2_sparql.convert(i)
-        graph.update(query)
diff --git a/parser/kgcl.lark b/parser/kgcl.lark
deleted file mode 100644
index 7b938f7a..00000000
--- a/parser/kgcl.lark
+++ /dev/null
@@ -1,74 +0,0 @@
-?expression: rename 
-           | create
-           | obsolete 
-           | delete 
-           | move
-           | create_class 
-           | unobsolete
-           | deepen
-           | shallow
-           | change_relationship
-           | create_edge
-           | delete_edge 
-           | create_synonym 
-           | remove_from_subset
-           | add_to_subset
-
-rename : "rename" old_label ["from" id] ("to"|"as"|"->") new_label 
-
-create : "create node" id label
-create_class: "create" id
-
-create_synonym: "create synonym" synonym "for" id
-
-delete : "delete" entity
-
-obsolete : "obsolete" entity
-         | "obsolete" entity "with replacement" replacement
-
-unobsolete: "unobsolete" id 
-
-deepen : "deepen" id "from" old_id ("to"|"->") new_id
-shallow : "shallow" id "from" old_id ("to"|"->") new_id 
-move : "move" id "from" old_id ("to"|"as"|"->") new_id 
-
-create_edge : "create edge" predicate "from" subject "to" object 
-delete_edge : "delete edge" predicate "from" subject "to" object 
-
-change_relationship: "change relationship between" subject "and" object "from" old "to" new 
-
-remove_from_subset :  "remove" id "from subset" subset
-add_to_subset :  "add" id "to subset" subset
-
-replacement : ID
-id : ID 
-subject: ID
-predicate: ID
-object: _entity
-subset: ID
-synonym : QUOTED_LABEL
-
-label : QUOTED_LABEL
-old_label : QUOTED_LABEL
-new_label : QUOTED_LABEL
-
-old_id : ID
-new_id : ID
-
-old : _entity
-new : _entity
-from : _entity 
-
-entity: _entity
-
-_entity : QUOTED_LABEL
-        | INNER_ID
-
-QUOTED_LABEL : "'" INNER_LABEL "'" 
-ID :  "<" INNER_ID ">"
-
-INNER_LABEL: /[^ \s\'].*?/ 
-INNER_ID: /\S+/
-
-%import common.WS
-%ignore WS
diff --git a/parser/kgcl_2_sparql.py b/parser/kgcl_2_sparql.py
deleted file mode 100644
index cbd6b299..00000000
--- a/parser/kgcl_2_sparql.py
+++ /dev/null
@@ -1,574 +0,0 @@
-import re
-from kgcl import (
-    NodeRename,
-    NodeObsoletion,
-    NodeUnobsoletion,
-    NodeDeletion,
-    NodeMove,
-    NodeDeepening,
-    NodeShallowing,
-    EdgeCreation,
-    EdgeDeletion,
-    PredicateChange,
-    NodeCreation,
-    ClassCreation,
-    NewSynonym,
-    RemovedNodeFromSubset,
-)
-
-
-def is_label(input):
-    return re.match(r"\'[^ \s\'].*\'", input)
-
-
-def is_id(input):
-    return re.match(r"<\S+>", input)
-
-
-def convert(kgclInstance):
-
-    # label renaming
-    # TODO: case for "rename 'old' from 'id' to 'new'
-    if type(kgclInstance) is NodeRename:
-        if is_label(kgclInstance.old_value) and is_label(kgclInstance.new_value):
-            return rename(kgclInstance)
-        # TODO: error handling
-
-    # node obsoletion
-    # TODO: new model only allows to obsolete a node
-    if type(kgclInstance) is NodeObsoletion:
-        if is_label(kgclInstance.about_node):
-            return obsolete_by_label(kgclInstance)
-        if is_id(kgclInstance.about_node):
-            return obsolete_by_id(kgclInstance)
-        # TODO: error handling
-
-    # node obsoletion
-    if type(kgclInstance) is NodeUnobsoletion:
-        if is_id(kgclInstance.about_node):
-            return unobsolete(kgclInstance)
-        # TODO: error handling
-
-    # node deletion
-    if type(kgclInstance) is NodeDeletion:
-        if is_id(kgclInstance.about_node):
-            return delete_by_id(kgclInstance)
-        if is_label(kgclInstance.about_node):
-            return delete_by_label(kgclInstance)
-        # TODO: error handling
-
-    # node creation
-    if type(kgclInstance) is NodeCreation:
-        if is_id(kgclInstance.node_id) and is_label(kgclInstance.name):
-            return create_node(kgclInstance)
-
-    # class creation
-    if type(kgclInstance) is ClassCreation:
-        if is_id(kgclInstance.node_id):
-            return create_class(kgclInstance)
-
-    # node deepending
-    if type(kgclInstance) is NodeDeepening:
-        if (
-            is_id(kgclInstance.about_edge.subject)
-            and is_id(kgclInstance.old_value)
-            and is_id(kgclInstance.new_value)
-        ):
-            return node_deepening(kgclInstance)
-
-    # node shallowing
-    if type(kgclInstance) is NodeShallowing:
-        if (
-            is_id(kgclInstance.about_edge.subject)
-            and is_id(kgclInstance.old_value)
-            and is_id(kgclInstance.new_value)
-        ):
-            return node_shallowing(kgclInstance)
-
-    # edge creation
-    if type(kgclInstance) is EdgeCreation:
-        if (
-            is_id(kgclInstance.subject)
-            and is_id(kgclInstance.predicate)
-            and (is_id(kgclInstance.object) or is_label(kgclInstance.object))
-        ):
-            return edge_creation(kgclInstance)
-
-    # edge deletion
-    if type(kgclInstance) is EdgeDeletion:
-        if (
-            is_id(kgclInstance.subject)
-            and is_id(kgclInstance.predicate)
-            and (is_id(kgclInstance.object) or is_label(kgclInstance.object))
-        ):
-            return edge_deletion(kgclInstance)
-
-    # node move
-    if type(kgclInstance) is NodeMove:
-        if (
-            is_id(kgclInstance.about_edge.subject)
-            and is_id(kgclInstance.old_value)
-            and is_id(kgclInstance.new_value)
-        ):
-            return node_move(kgclInstance)
-
-    if type(kgclInstance) is NewSynonym:
-        if is_id(kgclInstance.about_node) and is_label(kgclInstance.new_value):
-            return new_synonym(kgclInstance)
-
-    if type(kgclInstance) is PredicateChange:
-        if (
-            is_id(kgclInstance.about_edge.subject)
-            and is_id(kgclInstance.about_edge.object)
-            and is_id(kgclInstance.old_value)
-            and is_id(kgclInstance.new_value)
-        ):
-            return change_predicate(kgclInstance)
-
-    if type(kgclInstance) is RemovedNodeFromSubset:
-        if is_id(kgclInstance.about_node) and is_id(kgclInstance.subset):
-            return remove_node_from_subset(kgclInstance)
-
-
-def node_move(kgclInstance):
-    term_id = kgclInstance.about_edge.subject
-    old_value = kgclInstance.old_value
-    new_value = kgclInstance.new_value
-
-    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>  "
-    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
-    prefix += "PREFIX xsd:  <http://www.w3.org/2001/XMLSchema#> "
-
-    deleteQuery = term_id + " ?relation " + old_value + " . "
-
-    delete = "DELETE {" + deleteQuery + "}"
-
-    insertQuery = term_id + " ?relation " + new_value + " . "
-
-    insert = "INSERT {" + insertQuery + "}"
-
-    whereQuery = term_id + " ?relation " + old_value + " . "
-    where = "WHERE {" + whereQuery + "}"
-
-    updateQuery = prefix + " " + delete + " " + insert + " " + where
-
-    return updateQuery
-
-
-def remove_node_from_subset(kgclInstance):
-
-    about = kgclInstance.about_node
-    subset = kgclInstance.subset
-
-    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>  "
-    prefix += "PREFIX obo: <http://purl.obolibrary.org/obo/> "
-
-    deleteQuery = about + " obo:inSubset " + subset + " . "
-
-    delete = "DELETE {" + deleteQuery + "}"
-
-    whereQuery = about + " obo:inSubset " + subset + " . "
-
-    where = "WHERE { " + whereQuery + " }"
-
-    updateQuery = prefix + " " + delete + " " + where
-
-    return updateQuery
-
-
-def change_predicate(kgclInstance):
-
-    subject = kgclInstance.about_edge.subject
-    object = kgclInstance.about_edge.object
-
-    old_value = kgclInstance.old_value
-    new_value = kgclInstance.new_value
-
-    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>  "
-
-    deleteQuery = subject + " " + old_value + " " + object + " . "
-
-    delete = "DELETE {" + deleteQuery + "}"
-
-    insertQuery = subject + " " + new_value + " " + object + " . "
-
-    insert = "INSERT {" + insertQuery + "}"
-
-    where = "WHERE {}"
-
-    updateQuery = prefix + " " + delete + " " + insert + " " + where
-
-    return updateQuery
-
-
-def node_deepening(kgclInstance):
-
-    term_id = kgclInstance.about_edge.subject
-    old_value = kgclInstance.old_value
-    new_value = kgclInstance.new_value
-
-    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>  "
-    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
-    prefix += "PREFIX xsd:  <http://www.w3.org/2001/XMLSchema#> "
-
-    deleteQuery = term_id + " ?relation " + old_value + " . "
-
-    delete = "DELETE {" + deleteQuery + "}"
-
-    insertQuery = term_id + " ?relation " + new_value + " . "
-
-    insert = "INSERT {" + insertQuery + "}"
-
-    whereQuery = term_id + " ?relation " + old_value + " . "
-    whereQuery += new_value + " ?relation " + old_value + " . "
-    where = "WHERE {" + whereQuery + "}"
-
-    updateQuery = prefix + " " + delete + " " + insert + " " + where
-
-    return updateQuery
-
-
-def node_shallowing(kgclInstance):
-
-    term_id = kgclInstance.about_edge.subject
-    old_value = kgclInstance.old_value
-    new_value = kgclInstance.new_value
-
-    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>  "
-    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
-    prefix += "PREFIX xsd:  <http://www.w3.org/2001/XMLSchema#> "
-
-    deleteQuery = term_id + " ?relation " + old_value + " . "
-
-    delete = "DELETE {" + deleteQuery + "}"
-
-    insertQuery = term_id + " ?relation " + new_value + " . "
-
-    insert = "INSERT {" + insertQuery + "}"
-
-    whereQuery = term_id + " ?relation " + old_value + " . "
-    whereQuery += old_value + " ?relation " + new_value + " . "
-    where = "WHERE {" + whereQuery + "}"
-
-    updateQuery = prefix + " " + delete + " " + insert + " " + where
-
-    return updateQuery
-
-
-# TODO: handling of language tags
-# look things up at https://www.ebi.ac.uk/ols/ontologies/iao
-def unobsolete(kgclInstance):
-    about = kgclInstance.about_node
-    # http://wiki.geneontology.org/index.php/Restoring_an_Obsolete_Ontology_Term
-    # 1. remove 'obsolete' from label
-    # 2. remove 'OBSOLETE' from definition
-    # 3. update comment to "Note that this term was reinstated from obsolete"
-    #   TODO: no we remove the previous comment?  (all comments?)
-    # 4. Remove any replaced_by and consider tags
-    # 5. Remove the owl:deprecated: true tag
-
-    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>  "
-    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
-    prefix += "PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> "
-    prefix += "PREFIX obo: <http://purl.obolibrary.org/obo/> "
-    prefix += "PREFIX oboInOwl: <http://www.geneontology.org/formats/oboInOwl#> "
-
-    deleteQuery = about + " rdfs:label ?label . "
-    deleteQuery += about + ' owl:deprecated "true"^^xsd:boolean . '
-    deleteQuery += about + " obo:IAO_0000115 ?unobsolete_definition . "
-    deleteQuery += about + " obo:IAO_0100001 ?replacedBy .  "
-    deleteQuery += about + " oboInOwl:consider ?consider . "
-
-    delete = "DELETE {" + deleteQuery + "}"
-
-    insertQuery = about + " rdfs:label ?unobsolete_label . "
-    insertQuery += about + " obo:IAO_0000115 ?unobsolete_definition . "
-    insertQuery += (
-        '?entity rdfs:comment "Note that this term was reinstated from obsolete." . '
-    )
-
-    insert = "INSERT {" + insertQuery + "}"
-
-    whereQuery = "{ " + about + " rdfs:label ?label . "
-    whereQuery += 'BIND(IF(STRSTARTS(?label, "obsolete "),'
-    whereQuery += 'SUBSTR(?label,10),?label) AS ?unobsolete_label ) } '
-    whereQuery += " UNION "
-    whereQuery += "{ " + about + " rdfs:label ?label . "
-    whereQuery += about + " obo:IAO_0000115 ?definition . "
-    whereQuery += 'BIND(IF(STRSTARTS(?definition, "OBSOLETE "),'
-    whereQuery += 'SUBSTR(?definition,10),?definition) AS ?unobsolete_definition ) } '
-    whereQuery += " UNION "
-    whereQuery += "{ " + about + " rdfs:label ?label . "
-    whereQuery += about + " obo:IAO_0100001 ?replacedBy . } "
-    whereQuery += " UNION "
-    whereQuery += "{ " + about + " rdfs:label ?label . "
-    whereQuery += about + " oboInOwl:consider ?consider . } "
-
-    where = "WHERE {" + whereQuery + "}"
-
-    updateQuery = prefix + " " + delete + " " + insert + " " + where
-
-    return updateQuery
-
-
-def rename(kgclInstance):
-    oldValue = kgclInstance.old_value
-    newValue = kgclInstance.new_value
-    oldValue = oldValue.replace("'", "")
-
-    # initialise subject
-    if kgclInstance.about_node is None:
-        subject = "?entity"
-    else:
-        subject = kgclInstance.about_node
-
-    # this changes only the label of an entity
-    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>"
-    deleteQuery = subject + " rdfs:label ?label ."
-    delete = "DELETE {" + deleteQuery + "}"
-
-    insertQuery = subject + " rdfs:label " + newValue + " ."
-    insert = "INSERT {" + insertQuery + "}"
-
-    whereQuery = subject + " rdfs:label ?label .  "
-    whereQuery += " BIND( LANG(?label) AS ?language)  "
-    whereQuery += ' FILTER(STR(?label)="' + oldValue + '") '
-
-    where = "WHERE {" + whereQuery + "}"
-
-    updateQuery = prefix + " " + delete + " " + insert + " " + where
-
-    return updateQuery
-
-
-def delete_by_id(kgclInstance):
-    about = kgclInstance.about_node  # this needs to be an ID - not a label
-
-    deleteQuery = (
-        "?s1 ?p1 " + about + " . "
-    )  # this does not delete triples with blank nodes
-    deleteQuery += "?s2 " + about + " ?o1 . "
-    deleteQuery += "?s2 " + about + " ?o1 . "
-    deleteQuery += about + " ?p2 ?o2 . "
-
-    delete = "DELETE {" + deleteQuery + "}"
-
-    whereQuery = "{ ?s1 ?p1 " + about + " . } "
-    whereQuery += " UNION "
-    whereQuery = "{ ?s2 " + about + " ?o1 . } "
-    whereQuery += " UNION "
-    whereQuery += "{ " + about + " ?p2 ?o2 . } "
-
-    where = "WHERE {" + whereQuery + "}"
-
-    updateQuery = delete + " " + where
-
-    return updateQuery
-
-
-def delete_by_label(kgclInstance):
-    about = kgclInstance.about_node
-    about = about.replace("'", "")  # remove single quotes from label input
-
-    deleteQuery = "?s1 ?p1 ?label . "
-    deleteQuery += "?s1 ?p2 ?o1 . "
-
-    delete = "DELETE {" + deleteQuery + "}"
-
-    whereQuery = "?s1 ?p1 ?label . "
-    whereQuery += "?s1 ?p2 ?o1 . "
-    whereQuery += ' FILTER(STR(?label)="' + about + '") '  # ignore language tags
-
-    where = "WHERE {" + whereQuery + "}"
-
-    updateQuery = delete + " " + where
-
-    return updateQuery
-
-
-def create_class(kgclInstance):
-    termId = kgclInstance.node_id
-
-    prefix = "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>  "
-    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
-    insertQuery = termId + " rdf:type owl:Class  . "
-    insert = "INSERT {" + insertQuery + "}"
-    where = "WHERE {}"
-
-    updateQuery = prefix + " " + insert + " " + where
-
-    return updateQuery
-
-
-def create_node(kgclInstance):
-    termId = kgclInstance.node_id
-    label = kgclInstance.name
-
-    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>  "
-    insertQuery = termId + " rdfs:label " + label + "  . "
-    insert = "INSERT {" + insertQuery + "}"
-    where = "WHERE {}"
-
-    updateQuery = prefix + " " + insert + " " + where
-
-    return updateQuery
-
-
-def edge_creation(kgclInstance):
-    subject = kgclInstance.subject
-    predicate = kgclInstance.predicate
-    object = kgclInstance.object
-
-    prefix = "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>  "
-    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
-
-    insertQuery = subject + " " + predicate + " " + object + " . "
-    insert = "INSERT {" + insertQuery + "}"
-
-    where = "WHERE {}"
-
-    updateQuery = prefix + " " + insert + " " + where
-
-    return updateQuery
-
-
-def edge_deletion(kgclInstance):
-    subject = kgclInstance.subject
-    predicate = kgclInstance.predicate
-    object = kgclInstance.object
-
-    prefix = "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>  "
-    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
-
-    deleteQuery = subject + " " + predicate + " " + object + " . "
-    delete = "DELETE {" + deleteQuery + "}"
-
-    where = "WHERE {" + deleteQuery + "}"
-
-    updateQuery = prefix + " " + delete + " " + where
-
-    return updateQuery
-
-
-def obsolete_by_id(kgclInstance):
-    about = kgclInstance.about_node
-    replacement = kgclInstance.has_direct_replacement
-
-    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>  "
-    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
-    prefix += "PREFIX xsd:  <http://www.w3.org/2001/XMLSchema#> "
-    prefix += "PREFIX obo: <http://purl.obolibrary.org/obo/> "
-
-    deleteQuery = about + "rdfs:label ?label . "
-    deleteQuery += about + " rdfs:subClassOf ?superclass . "
-    deleteQuery += about + " owl:equivalentClass ?rhs . "
-    deleteQuery += "?lhs owl:equivalentClass " + about + " ."
-
-    delete = "DELETE {" + deleteQuery + "}"
-
-    insertQuery = "?entity rdfs:label ?tag . "
-    insertQuery += about + ' owl:deprecated "true"^^xsd:boolean . '
-    if kgclInstance.has_direct_replacement is not None:
-        insertQuery += about + " obo:IAO_0100001 " + replacement + "  .  "
-
-    insert = "INSERT {" + insertQuery + "}"
-
-    whereQuery = "{ " + about + " rdfs:subClassOf ?superclass . } "
-    whereQuery += " UNION "
-    whereQuery += "{ " + about + " owl:equivalentClass ?rhs . } "
-    whereQuery += " UNION "
-    whereQuery += "{ ?lhs owl:equivalentClass " + about + " . } "
-    whereQuery += " UNION "
-    whereQuery += "{ ?entity rdfs:label ?label . "
-    whereQuery += ' BIND(CONCAT("obsolete ", ?label) AS ?obsolete_label )  '
-    whereQuery += " BIND( LANG(?label) AS ?language)  "
-    whereQuery += " BIND( STRLANG(?obsolete_label,?language) AS ?tag) }  "
-
-    where = "WHERE {" + whereQuery + "}"
-
-    updateQuery = prefix + " " + delete + " " + insert + " " + where
-
-    return updateQuery
-
-
-def obsolete_by_label(kgclInstance):
-    about = kgclInstance.about_node
-
-    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>  "
-    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
-    prefix += "PREFIX xsd:  <http://www.w3.org/2001/XMLSchema#> "
-
-    deleteQuery = "?entity rdfs:label ?label . "
-    deleteQuery += "?entity rdfs:subClassOf ?superclass . "
-    deleteQuery += "?entity owl:equivalentClass ?rhs . "
-    deleteQuery += "?lhs owl:equivalentClass ?entity . "
-
-    delete = "DELETE {" + deleteQuery + "}"
-
-    inner_label = about.replace("'", "")
-    obsolete_label = "obsolete " + inner_label
-
-    insertQuery = "?entity rdfs:label ?tag . "
-    insertQuery += '?entity owl:deprecated "true"^^xsd:boolean . '
-
-    insert = "INSERT {" + insertQuery + "}"
-
-    # TODO: handle the special case where only a label is present
-    # (in that case we need to query for a single triple)
-    whereQuery = "{ ?entity rdfs:label ?label .  "
-    whereQuery += " ?entity rdfs:subClassOf ?superclass .  "
-    whereQuery += " BIND( LANG(?label) AS ?language)  "
-    whereQuery += ' BIND( STRLANG("' + obsolete_label + '",?language) AS ?tag)  '
-    whereQuery += ' FILTER(STR(?label)="' + inner_label + '") } '
-
-    whereQuery += " UNION "
-
-    whereQuery += "{ ?entity rdfs:label ?label .  "
-    whereQuery += " ?entity owl:equivalentClass ?rhs . "
-    whereQuery += " BIND( LANG(?label) AS ?language) "
-    whereQuery += ' BIND( STRLANG("' + obsolete_label + '",?language) AS ?tag)  '
-    whereQuery += ' FILTER(STR(?label)="' + inner_label + '") } '
-
-    whereQuery += " UNION "
-
-    whereQuery += "{ ?entity rdfs:label ?label .  "
-    whereQuery += " ?lhs owl:equivalentClass ?entity . "
-    whereQuery += " BIND( LANG(?label) AS ?language) "
-    whereQuery += ' BIND( STRLANG("' + obsolete_label + '",?language) AS ?tag)  '
-    whereQuery += ' FILTER(STR(?label)="' + inner_label + '") } '
-
-    whereQuery += " UNION "
-
-    whereQuery += "{ ?entity rdfs:label ?label .  "
-    whereQuery += " ?entity rdf:type ?type .  "
-    whereQuery += " BIND( LANG(?label) AS ?language) "
-    whereQuery += ' BIND( STRLANG("' + obsolete_label + '",?language) AS ?tag)  '
-    whereQuery += ' FILTER(STR(?label)="' + inner_label + '") } '
-
-    where = "WHERE {" + whereQuery + "}"
-
-    updateQuery = prefix + " " + delete + " " + insert + " " + where
-
-    return updateQuery
-
-
-def new_synonym(kgclInstance):
-    about = kgclInstance.about_node
-    synonym = kgclInstance.new_value
-
-    prefix = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>  "
-    prefix += "PREFIX owl: <http://www.w3.org/2002/07/owl#>  "
-    prefix += "PREFIX xsd:  <http://www.w3.org/2001/XMLSchema#> "
-    prefix += "PREFIX oboInOwl: <http://www.geneontology.org/formats/oboInOwl#> "
-
-    # TODO: check whether this way of creating synonyms is OK
-    # or whether we want to include qualifiers, e.g. broader, exact, related, narrower
-    insertQuery = about + " oboInOwl:Synonym " + synonym + " . "
-
-    insert = "INSERT {" + insertQuery + "}"
-
-    where = "WHERE {}"
-
-    updateQuery = prefix + " " + insert + " " + where
-
-    return updateQuery
diff --git a/parser/parser.py b/parser/parser.py
deleted file mode 100644
index 6dfbfd07..00000000
--- a/parser/parser.py
+++ /dev/null
@@ -1,287 +0,0 @@
-from lark import Lark, Token
-from kgcl import (
-    NodeRename,
-    NodeObsoletion,
-    NodeUnobsoletion,
-    NodeDeletion,
-    NodeMove,
-    NodeDeepening,
-    NodeShallowing,
-    EdgeCreation,
-    EdgeDeletion,
-    PredicateChange,
-    NodeCreation,
-    ClassCreation,
-    NewSynonym,
-    RemovedNodeFromSubset,
-)
-from ontology_model import Edge
-from pathlib import Path
-
-
-def id_generator():
-    id = 0
-    while True:
-        yield id
-        id += 1
-
-
-path = Path(__file__).parent
-kgcl_parser = Lark.open(str(path) + "/kgcl.lark", start="expression")
-id_gen = id_generator()
-
-
-# input may be a set of KGCL statements separated by \n
-def parse(input):
-    statements = input.splitlines()
-    parsed = []
-
-    for s in statements:
-        parsed.append(parse_statement(s))
-        # parsed.append((s,parse_statement(s)))
-    return parsed
-
-
-def parse_statement(input):
-
-    tree = kgcl_parser.parse(input)
-
-    id = "test_id_" + str(next(id_gen))
-
-    command = tree.data
-    # print("Command: " + command)
-
-    if command == "rename":
-        # node with data 'old_label' is unique
-        old = next(tree.find_data("old_label"))
-        # and this node only has one token
-        old_token = next(get_tokens(old))
-
-        new = next(tree.find_data("new_label"))
-        new_token = next(get_tokens(new))
-
-        term_id = tree.find_data("id")
-
-        term_id_list = list(tree.find_data("id"))
-        # test whether there is an element in the generator
-        if term_id_list:
-            t = next(term_id)
-            term_id_token = next(get_tokens(t))
-            return NodeRename(
-                id=id,
-                about_node=term_id_token,
-                old_value=old_token,
-                new_value=new_token,
-            )
-        else:
-            return NodeRename(id=id, old_value=old_token, new_value=new_token)
-
-    if command == "obsolete":
-        label = next(tree.find_data("entity"))
-        label_token = next(get_tokens(label))
-
-        replacement_id = tree.find_data("replacement")
-
-        replacement_id_list = list(tree.find_data("replacement"))
-        if replacement_id_list:
-            t = next(replacement_id)
-            replace = next(get_tokens(t))
-            return NodeObsoletion(
-                id=id, about_node=label_token, has_direct_replacement=replace
-            )
-        else:
-            return NodeObsoletion(id=id, about_node=label_token)
-
-    if command == "unobsolete":
-        term_id = next(tree.find_data("id"))
-        term_id_token = next(get_tokens(term_id))
-
-        return NodeUnobsoletion(id=id, about_node=term_id_token)
-
-    if command == "delete":
-        label = next(
-            tree.find_data("entity")
-        )  # TODO check whether we want to delete nodes by label
-        label_token = next(get_tokens(label))
-
-        return NodeDeletion(id=id, about_node=label_token)
-
-    if command == "move":
-        term_id = next(tree.find_data("id"))
-        term_id_token = next(get_tokens(term_id))
-
-        old = next(tree.find_data("old_id"))
-        old_token = next(get_tokens(old))
-
-        new = next(tree.find_data("new_id"))
-        new_token = next(get_tokens(new))
-
-        edge = Edge(subject=term_id_token, object=old_token)
-
-        return NodeMove(
-            id=id, about_edge=edge, old_value=old_token, new_value=new_token
-        )
-    if command == "deepen":
-        term_id = next(tree.find_data("id"))
-        term_id_token = next(get_tokens(term_id))
-
-        old = next(tree.find_data("old_id"))
-        old_token = next(get_tokens(old))
-
-        new = next(tree.find_data("new_id"))
-        new_token = next(get_tokens(new))
-
-        edge = Edge(subject=term_id_token, object=old_token)
-
-        return NodeDeepening(
-            id=id, about_edge=edge, old_value=old_token, new_value=new_token
-        )
-
-    if command == "shallow":
-        term_id = next(tree.find_data("id"))
-        term_id_token = next(get_tokens(term_id))
-
-        old = next(tree.find_data("old_id"))
-        old_token = next(get_tokens(old))
-
-        new = next(tree.find_data("new_id"))
-        new_token = next(get_tokens(new))
-
-        edge = Edge(subject=term_id_token, object=old_token)
-
-        return NodeShallowing(
-            id=id, about_edge=edge, old_value=old_token, new_value=new_token
-        )
-
-    if command == "create_edge":
-        subject = next(tree.find_data("subject"))
-        subject_token = next(get_tokens(subject))
-
-        predicate = next(tree.find_data("predicate"))
-        predicate_token = next(get_tokens(predicate))
-
-        object = next(tree.find_data("object"))
-        object_token = next(get_tokens(object))
-
-        return EdgeCreation(
-            id=id, subject=subject_token, predicate=predicate_token, object=object_token
-        )
-
-    if command == "delete_edge":
-        subject = next(tree.find_data("subject"))
-        subject_token = next(get_tokens(subject))
-
-        predicate = next(tree.find_data("predicate"))
-        predicate_token = next(get_tokens(predicate))
-
-        object = next(tree.find_data("object"))
-        object_token = next(get_tokens(object))
-
-        return EdgeDeletion(
-            id=id, subject=subject_token, predicate=predicate_token, object=object_token
-        )
-
-    if command == "change_relationship":
-        subject = next(tree.find_data("subject"))
-        subject_token = next(get_tokens(subject))
-
-        object = next(tree.find_data("object"))
-        object_token = next(get_tokens(object))
-
-        edge = Edge(subject=subject_token, object=object_token)
-
-        old = next(tree.find_data("old"))
-        old_token = next(get_tokens(old))
-
-        new = next(tree.find_data("new"))
-        new_token = next(get_tokens(new))
-        return PredicateChange(
-            id=id, about_edge=edge, old_value=old_token, new_value=new_token
-        )
-
-    # the KGCL model suggests the command
-    # 'create node {id} {label} with {annotation set}'
-    # TODO: handling of {annotation set}
-    if command == "create":
-        term_id = next(tree.find_data("id"))
-        term_id_token = next(get_tokens(term_id))
-
-        label = next(tree.find_data("label"))
-        label_token = next(get_tokens(label))
-        # TODO: where is the difference between
-        # a 'node_id' provided by 'NodeCreation'
-        # and 'about_node' inherited by 'NodeChange'?
-        return NodeCreation(
-            id=id, about_node=term_id_token, node_id=term_id_token, name=label_token
-        )
-
-    if command == "create_class":
-        term_id = next(tree.find_data("id"))
-        term_id_token = next(get_tokens(term_id))
-        return ClassCreation(id=id, node_id=term_id_token)
-
-    if command == "create_synonym":
-        term_id = next(tree.find_data("id"))
-        term_id_token = next(get_tokens(term_id))
-
-        # synonym_type = next(tree.find_data('synonym_type'))
-        # synonym_type_token = next(get_tokens(synonym_type))
-
-        synonym_string = next(tree.find_data("synonym"))
-        synonym_string_token = next(get_tokens(synonym_string))
-
-        return NewSynonym(
-            id=id, about_node=term_id_token, new_value=synonym_string_token
-        )
-    # TODO: does not have a field for subsets
-    # if(command == "add_to_subset"):
-
-    #    term_id = next(tree.find_data('id'))
-    #    term_id_token = next(get_tokens(term_id))
-
-    #    subset_id = next(tree.find_data('subset'))
-    #    subset_id_token = next(get_tokens(subset_id))
-
-    #    return AddNodeToSubset(
-    #        id=id, in_subset=subset_id_token, about_node=term_id_token
-    #     )
-
-    if command == "remove_from_subset":
-
-        term_id = next(tree.find_data("id"))
-        term_id_token = next(get_tokens(term_id))
-
-        subset_id = next(tree.find_data("subset"))
-        subset_id_token = next(get_tokens(subset_id))
-
-        return RemovedNodeFromSubset(
-            id=id, subset=subset_id_token, about_node=term_id_token
-        )
-
-    # TODO: more cases
-    # if(command == "merge"):
-    # if(command == "add"):
-    # if(command == "add_parent"):
-    # if(command == "add_annotation"):
-    # if(command == "add_synonym"):
-    # if(command == "add_axiom"): (in Manchester Syntax?)
-    # if(command == "add_class"):
-    # if(command == "add_text_definition"):
-    # if(command == "merge"):
-
-
-def get_tokens(tree):
-    return tree.scan_values(lambda v: isinstance(v, Token))
-
-
-if __name__ == "__main__":
-    #
-    # MANUAL TESTING
-    #
-    example1 = "renamea 'abnormal ear' to 'abnormal ear morphology'"
-    tree = kgcl_parser.parse(example1)
-    print(tree)
-    old = next(tree.find_data("old_label"))
-    print(old)
-    old_token = next(get_tokens(old))
-    print(old_token)
diff --git a/parser/prov.py b/parser/prov.py
deleted file mode 100644
index 74018c27..00000000
--- a/parser/prov.py
+++ /dev/null
@@ -1,159 +0,0 @@
-# Auto generated from prov.yaml by pythongen.py version: 0.9.0
-# Generation date: 2021-07-16 19:00
-# Schema: KGCL-PROV
-#
-# id: https://w3id.org/kgcl/prov
-# description: See https://www.w3.org/TR/prov-o/
-# license: https://creativecommons.org/publicdomain/zero/1.0/
-
-import dataclasses
-import sys
-import re
-from jsonasobj2 import JsonObj
-from typing import Optional, List, Union, Dict, ClassVar, Any
-from dataclasses import dataclass
-from linkml_runtime.linkml_model.meta import EnumDefinition, PermissibleValue, PvFormulaOptions
-
-from linkml_runtime.utils.slot import Slot
-from linkml_runtime.utils.metamodelcore import empty_list, empty_dict, bnode
-from linkml_runtime.utils.yamlutils import YAMLRoot, extended_str, extended_float, extended_int
-from linkml_runtime.utils.dataclass_extensions_376 import dataclasses_init_fn_with_kwargs
-from linkml_runtime.utils.formatutils import camelcase, underscore, sfx
-from linkml_runtime.utils.enumerations import EnumDefinitionImpl
-from rdflib import Namespace, URIRef
-from linkml_runtime.utils.curienamespace import CurieNamespace
-from linkml_runtime.linkml_model.types import String
-
-metamodel_version = "1.7.0"
-
-# Overwrite dataclasses _init_fn to add **kwargs in __init__
-dataclasses._init_fn = dataclasses_init_fn_with_kwargs
-
-# Namespaces
-LINKML = CurieNamespace('linkml', 'https://w3id.org/linkml/')
-PROV = CurieNamespace('prov', 'http://www.w3.org/ns/prov#')
-DEFAULT_ = PROV
-
-
-# Types
-
-# Class references
-class ActivityId(extended_str):
-    pass
-
-
-class AgentId(extended_str):
-    pass
-
-
-@dataclass
-class Activity(YAMLRoot):
-    """
-    a provence-generating activity
-    """
-    _inherited_slots: ClassVar[List[str]] = []
-
-    class_class_uri: ClassVar[URIRef] = PROV.Activity
-    class_class_curie: ClassVar[str] = "prov:Activity"
-    class_name: ClassVar[str] = "activity"
-    class_model_uri: ClassVar[URIRef] = PROV.Activity
-
-    id: Union[str, ActivityId] = None
-    started_at_time: Optional[str] = None
-    ended_at_time: Optional[str] = None
-    was_informed_by: Optional[Union[str, ActivityId]] = None
-    was_associated_with: Optional[Union[str, AgentId]] = None
-    used: Optional[str] = None
-    description: Optional[str] = None
-
-    def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
-        if self._is_empty(self.id):
-            self.MissingRequiredField("id")
-        if not isinstance(self.id, ActivityId):
-            self.id = ActivityId(self.id)
-
-        if self.started_at_time is not None and not isinstance(self.started_at_time, str):
-            self.started_at_time = str(self.started_at_time)
-
-        if self.ended_at_time is not None and not isinstance(self.ended_at_time, str):
-            self.ended_at_time = str(self.ended_at_time)
-
-        if self.was_informed_by is not None and not isinstance(self.was_informed_by, ActivityId):
-            self.was_informed_by = ActivityId(self.was_informed_by)
-
-        if self.was_associated_with is not None and not isinstance(self.was_associated_with, AgentId):
-            self.was_associated_with = AgentId(self.was_associated_with)
-
-        if self.used is not None and not isinstance(self.used, str):
-            self.used = str(self.used)
-
-        if self.description is not None and not isinstance(self.description, str):
-            self.description = str(self.description)
-
-        super().__post_init__(**kwargs)
-
-
-@dataclass
-class Agent(YAMLRoot):
-    """
-    a provence-generating agent
-    """
-    _inherited_slots: ClassVar[List[str]] = []
-
-    class_class_uri: ClassVar[URIRef] = PROV.Agent
-    class_class_curie: ClassVar[str] = "prov:Agent"
-    class_name: ClassVar[str] = "agent"
-    class_model_uri: ClassVar[URIRef] = PROV.Agent
-
-    id: Union[str, AgentId] = None
-    acted_on_behalf_of: Optional[Union[str, AgentId]] = None
-    was_informed_by: Optional[Union[str, ActivityId]] = None
-
-    def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
-        if self._is_empty(self.id):
-            self.MissingRequiredField("id")
-        if not isinstance(self.id, AgentId):
-            self.id = AgentId(self.id)
-
-        if self.acted_on_behalf_of is not None and not isinstance(self.acted_on_behalf_of, AgentId):
-            self.acted_on_behalf_of = AgentId(self.acted_on_behalf_of)
-
-        if self.was_informed_by is not None and not isinstance(self.was_informed_by, ActivityId):
-            self.was_informed_by = ActivityId(self.was_informed_by)
-
-        super().__post_init__(**kwargs)
-
-
-# Enumerations
-
-
-# Slots
-class slots:
-    pass
-
-slots.started_at_time = Slot(uri=PROV.startedAtTime, name="started at time", curie=PROV.curie('startedAtTime'),
-                   model_uri=PROV.started_at_time, domain=None, range=Optional[str])
-
-slots.ended_at_time = Slot(uri=PROV.endedAtTime, name="ended at time", curie=PROV.curie('endedAtTime'),
-                   model_uri=PROV.ended_at_time, domain=None, range=Optional[str])
-
-slots.was_informed_by = Slot(uri=PROV.wasInformedBy, name="was informed by", curie=PROV.curie('wasInformedBy'),
-                   model_uri=PROV.was_informed_by, domain=None, range=Optional[Union[str, ActivityId]])
-
-slots.was_associated_with = Slot(uri=PROV.wasAssociatedWith, name="was associated with", curie=PROV.curie('wasAssociatedWith'),
-                   model_uri=PROV.was_associated_with, domain=None, range=Optional[Union[str, AgentId]])
-
-slots.acted_on_behalf_of = Slot(uri=PROV.actedOnBehalfOf, name="acted on behalf of", curie=PROV.curie('actedOnBehalfOf'),
-                   model_uri=PROV.acted_on_behalf_of, domain=None, range=Optional[Union[str, AgentId]])
-
-slots.was_generated_by = Slot(uri=PROV.wasGeneratedBy, name="was generated by", curie=PROV.curie('wasGeneratedBy'),
-                   model_uri=PROV.was_generated_by, domain=None, range=Optional[Union[str, ActivityId]])
-
-slots.used = Slot(uri=PROV.used, name="used", curie=PROV.curie('used'),
-                   model_uri=PROV.used, domain=Activity, range=Optional[str])
-
-slots.activity_set = Slot(uri=PROV.activity_set, name="activity set", curie=PROV.curie('activity_set'),
-                   model_uri=PROV.activity_set, domain=None, range=Optional[Union[Dict[Union[str, ActivityId], Union[dict, Activity]], List[Union[dict, Activity]]]])
-
-slots.agent_set = Slot(uri=PROV.agent_set, name="agent set", curie=PROV.curie('agent_set'),
-                   model_uri=PROV.agent_set, domain=None, range=Optional[Union[Dict[Union[str, AgentId], Union[dict, Agent]], List[Union[dict, Agent]]]])
diff --git a/parser/requirements.txt b/parser/requirements.txt
deleted file mode 100644
index f45fc4ec..00000000
--- a/parser/requirements.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-linkml >= 1.0.0
-flask
-lark 
diff --git a/setup.py b/setup.py
new file mode 100644
index 00000000..63991bc4
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,36 @@
+from setuptools import setup
+
+with open("README.md", "r") as fh:
+    long_description = fh.read()
+
+setup(
+    name="kgcl",
+    version="0.0.10",
+    description="Knowledge Graph Change Language",
+    packages=[
+        "kgcl.apply",
+        "kgcl.diff",
+        "kgcl.grammar",
+        "kgcl.model",
+    ],
+    # py_modules=[
+    #    "kgcl",
+    #    "kgcl_diff",
+    #    "pretty_print_kgcl",
+    # ],
+    package_dir={"kgcl": "./kgcl"},
+    package_data={"kgcl.grammar": ["kgcl.lark"]},
+    classifiers=[
+        "Programming Language :: Python :: 3.8",
+    ],
+    long_description=long_description,
+    long_description_content_type="text/markdown",
+    install_requires=[
+        "linkml ~= 1.0.3",
+        "lark ~= 0.11.3",
+    ],
+    extras_require={"dev": ["pytest>=3.7"]},
+    url="https://github.com/ckindermann/knowledge-graph-change-language",
+    author="Christian Kindermann",
+    author_email="chris.kind.man@gmail.com",
+),
diff --git a/src/schema/kgcl.yaml b/src/schema/kgcl.yaml
index cb9aef93..f2410fb2 100644
--- a/src/schema/kgcl.yaml
+++ b/src/schema/kgcl.yaml
@@ -74,6 +74,12 @@ classes:
     slots:
       - old value
       - new value
+      - old value type
+      - new value type
+      - new language
+      - old language
+      - new datatype
+      - old datatype
 
   complex change:
     abstract: true
@@ -255,6 +261,9 @@ classes:
       A change in which the element that is the focus of the change is an edge.
     slots:
       - about edge
+      - object type
+      - language
+      - datatype
     slot_usage:
       subject:
         comments:
@@ -274,6 +283,9 @@ classes:
       - subject
       - predicate
       - object
+      - subject type
+      - predicate type
+      - object type
       - annotation set
     slot_usage:
       change description:
@@ -298,10 +310,18 @@ classes:
       - subject
       - predicate
       - object
+      - subject type
+      - predicate type
+      - object type
       - annotation set
     slot_usage:
       change description:
         string_serialization: "deleting edge {edge}"
+
+  remove under:
+    is_a: edge deletion
+    description: >-
+      An edge deletion where the predicate is owl:subClassOf
         
   edge obsoletion:
     aliases:
@@ -363,6 +383,9 @@ classes:
     is_a: edge change
     description: >-
       A node move is a combination of deleting a parent edge and adding a parent edge, where the predicate is preserved and the object/parent node changes
+    slots:
+      - old object type
+      - new object type
     examples:
       - value: changing a is-a b to a is-a c
     slot_usage:
@@ -411,6 +434,7 @@ classes:
     is_a: simple change
     description: >-
       A simple change where a logical axiom is changed, where the logical axiom cannot be represented as an edge
+
         
   node change:
     abstract: true
@@ -419,6 +443,7 @@ classes:
       A simple change where the change is about a node
     slots:
       - about node
+      - about node representation
       - language
 
   node rename:
@@ -429,6 +454,9 @@ classes:
       - old value
       - new value
       - has textual diff      
+      - new language
+      - old language
+
     slot_usage:
       old value:
         multivalued: false
@@ -457,6 +485,9 @@ classes:
     description: >-
       A node change where the change alters node properties/annotations. TODO
     is_a: node change
+    slots:
+      - annotation property
+      - annotation property type
 
   node annotation replacement:
     is_a: node annotation change
@@ -472,6 +503,8 @@ classes:
     is_a: node synonym change
     slots:
       - new value
+      - language
+      - qualifier
 
   name becomes synonym:
     is_a: node synonym change
@@ -636,6 +669,7 @@ classes:
       - name
       - owl type
       - annotation set
+      - language
     slot_usage:
       change description:
         string_serialization: "creating node {id} {label} with {annotation set}"
@@ -787,6 +821,10 @@ slots:
     multivalued: false
     range: edge
 
+  about node representation:
+    description: >-
+      The representation of a node (URI, CURIE, label) 
+
   target:
     description: >-
       The secondary entity on which the change operates
@@ -799,11 +837,99 @@ slots:
     description: >-
       The value of a property held in the new instance of the ontology
 
+  language:
+    description: >-
+      The language tag of a literal
+
+  datatype:
+    description: >-
+      The datatype of a literal
+
+  new datatype:
+    description: >-
+      The new datatype of a literal
+
+  old datatype:
+    description: >-
+      The old datatype of a literal
+
+  new language:
+    description: >-
+      The new language tag of a literal
+
+  old language:
+    description: >-
+      The old language tag of a literal
+
+  qualifier:
+    description: >-
+      The qualifier of a change operation
+
+  subclass:
+    description: >-
+      The subclass of a subsumption axiom
+
+  new subclass:
+    description: >-
+      The new subclass of a subsumption axiom
+
+  new property:
+    description: >-
+      The new property of an OWL restriction
+
+  new filler:
+    description: >-
+      The new filler of an OWL restriction
+
+  object type:
+    description: >-
+      The type (IRI or Literal) of an object
+
+  new object type:
+    description: >-
+      The type (IRI or Literal) of a new object
+
+  old object type:
+    description: >-
+      The type (IRI or Literal) of an old object
+
+  new value type:
+    description: >-
+      The type (IRI or Literal) of a new value
+
+  old value type:
+    description: >-
+      The type (IRI or Literal) of an old value
+
+  subject type:
+    description: >-
+      The type (IRI or Literal) of an edge's subject
+
+  subclass type:
+    description: >-
+      The type of a subclass
+
+  superclass type:
+    description: >-
+      The type of a superclass
+
+  predicate type:
+    description: >-
+      The type (IRI or Literal) of an edge's subject
+
   in subset:
     description: >-
       The subset that pertains to this change
     range: ontology subset
 
+  annotation property:
+    description: >-
+      An annotation property
+
+  annotation property type:
+    description: >-
+      The type of a property (URI, CURIE, label) 
+
   change description:
     description: >-
       A string serialization of the change. This should be both human-readable, and parseable.
diff --git a/src/schema/ontology_model.yaml b/src/schema/ontology_model.yaml
index 14d009de..07dcb532 100644
--- a/src/schema/ontology_model.yaml
+++ b/src/schema/ontology_model.yaml
@@ -45,6 +45,8 @@ classes:
       - property
       - filler
       - annotation set
+      - property type
+      - filler type
     mappings:
       - owl:Annotation
       
@@ -92,6 +94,9 @@ classes:
       - subject
       - predicate
       - object
+      - subject representation
+      - predicate representation
+      - object representation
       - annotation set
 
   logical definition:
@@ -125,6 +130,16 @@ slots:
 
   filler: {}
 
+  property type: {}
+
+  filler type: {}
+
+  subject representation: {}
+
+  predicate representation: {}
+
+  object representation: {}
+
   property value set:
     range: property value
     multivalued: true
diff --git a/parser/static/css/main.css b/static/css/main.css
similarity index 100%
rename from parser/static/css/main.css
rename to static/css/main.css
diff --git a/parser/templates/base.html b/templates/base.html
similarity index 100%
rename from parser/templates/base.html
rename to templates/base.html
diff --git a/templates/diff.html b/templates/diff.html
new file mode 100644
index 00000000..673c6ba0
--- /dev/null
+++ b/templates/diff.html
@@ -0,0 +1,31 @@
+{% extends 'base.html' %}
+
+{% block head %}
+{% endblock %}
+
+{% block body %} 
+<h1>KGCL Web App (Demo)</h1>
+
+<div class="content">
+    <form action="/diff" method="POST">
+        <!-- <input type="text" name="content" id="content"> -->
+        <p><b>Graph 1</b></p>
+        <textarea name="graph1" id="content" rows="10" cols="30">{{ inputGraph1 }}</textarea>
+        <p><b>Graph 2</b></p>
+        <textarea name="graph2" id="content" rows="10" cols="30">{{ inputGraph2 }}</textarea>
+        <input type="submit" name="generate_diff" value="Generate Diff"> 
+        <input type="submit" name="load_example_diff" value="Load Example"> 
+
+        <select name="comp_select" class="selectpicker form-control">
+          {% for o in examples %}
+              <!-- <option value="{{ o.name }}">{{ o.name }}</option> -->
+               <option value="{{ o }}">{{ o }}</option>
+          {% endfor %}
+        </select>
+    </form> 
+
+    <p><b>KGCL Diff:</b> <br>{{ kgclDiff }}</p>
+</div> 
+<p> <a href="{{ url_for('index') }}">Test KGCL</a> </p>
+
+{% endblock %}
diff --git a/parser/templates/index.html b/templates/index.html
similarity index 95%
rename from parser/templates/index.html
rename to templates/index.html
index 6747ff7e..1fc52fe2 100644
--- a/parser/templates/index.html
+++ b/templates/index.html
@@ -27,5 +27,6 @@ <h1>KGCL Web App (Demo)</h1>
     <p><b>KGCL Model:</b> <br>{{ parsedKGCL }}</p>
     <p><b>Output Graph:</b> <br>{{ outputGraph }}</p>
 </div> 
+    <p> <a href="diff">Test KGCL Diff</a> </p>
 
 {% endblock %}


Test patch:
diff --git a/kgcl/tests/__init__.py b/kgcl/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/kgcl/tests/__pycache__/test_model.cpython-38-pytest-6.2.4.pyc b/kgcl/tests/__pycache__/test_model.cpython-38-pytest-6.2.4.pyc
new file mode 100644
index 00000000..18b75ca7
Binary files /dev/null and b/kgcl/tests/__pycache__/test_model.cpython-38-pytest-6.2.4.pyc differ
diff --git a/kgcl/tests/test_apply/__init__.py b/kgcl/tests/test_apply/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/kgcl/tests/test_apply/__pycache__/test_create_edge.cpython-38-pytest-6.2.4.pyc b/kgcl/tests/test_apply/__pycache__/test_create_edge.cpython-38-pytest-6.2.4.pyc
new file mode 100644
index 00000000..12ebede0
Binary files /dev/null and b/kgcl/tests/test_apply/__pycache__/test_create_edge.cpython-38-pytest-6.2.4.pyc differ
diff --git a/kgcl/tests/test_apply/__pycache__/test_with_pytest.cpython-38-pytest-6.2.4.pyc b/kgcl/tests/test_apply/__pycache__/test_with_pytest.cpython-38-pytest-6.2.4.pyc
new file mode 100644
index 00000000..e78b974c
Binary files /dev/null and b/kgcl/tests/test_apply/__pycache__/test_with_pytest.cpython-38-pytest-6.2.4.pyc differ
diff --git a/kgcl/tests/test_apply/test_change_predicate.py b/kgcl/tests/test_apply/test_change_predicate.py
new file mode 100644
index 00000000..09cb0737
--- /dev/null
+++ b/kgcl/tests/test_apply/test_change_predicate.py
@@ -0,0 +1,31 @@
+from kgcl.tests.test_apply.util import run_test
+
+
+def test_change_predicate_with_ids():
+    input_graph = "<http://example.org/subclass> <http://example.org/partOf> <http://example.org/superclass> ."
+    kgcl_patch = "change relationship between <http://example.org/subclass> and <http://example.org/superclass> from <http://example.org/partOf> to <http://example.org/hasPart>"
+    expected_graph = "<http://example.org/subclass> <http://example.org/hasPart> <http://example.org/superclass> ."
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_change_predicate_with_curies():
+    input_graph = "<http://example.org/subclass> <http://example.org/partOf> <http://example.org/superclass> ."
+    kgcl_patch = "change relationship between ex:subclass and ex:superclass from ex:partOf to ex:hasPart"
+    expected_graph = "<http://example.org/subclass> <http://example.org/hasPart> <http://example.org/superclass> ."
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_change_predicate_with_labels():
+    input_graph = """<http://example.org/subclass> <http://example.org/partOf> <http://example.org/superclass> .  
+                     <http://example.org/subclass> <http://www.w3.org/2000/01/rdf-schema#label> "subclass" .
+                     <http://example.org/superclass> <http://www.w3.org/2000/01/rdf-schema#label> "superclass" .
+                     <http://example.org/hasPart> <http://www.w3.org/2000/01/rdf-schema#label> "hasPart" ."""
+    kgcl_patch = "change relationship between ex:subclass and ex:superclass from ex:partOf to ex:hasPart"
+    expected_graph = """<http://example.org/subclass> <http://example.org/hasPart> <http://example.org/superclass> .
+                        <http://example.org/subclass> <http://www.w3.org/2000/01/rdf-schema#label> "subclass" .
+                        <http://example.org/superclass> <http://www.w3.org/2000/01/rdf-schema#label> "superclass" .
+                        <http://example.org/hasPart> <http://www.w3.org/2000/01/rdf-schema#label> "hasPart" ."""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
diff --git a/kgcl/tests/test_apply/test_create_class.py b/kgcl/tests/test_apply/test_create_class.py
new file mode 100644
index 00000000..737fd081
--- /dev/null
+++ b/kgcl/tests/test_apply/test_create_class.py
@@ -0,0 +1,19 @@
+from kgcl.tests.test_apply.util import run_test
+
+
+def test_create_edge_with_ids():
+    input_graph = ""
+    kgcl_patch = "create <http://purl.obolibrary.org/obo/NCBITaxon_2>"
+    expected_graph = "<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Class> ."
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_create_edge_with_label():
+    input_graph = ""
+    kgcl_patch = (
+        "create node <http://purl.obolibrary.org/obo/NCBITaxon_2> 'Bacteria'@en"
+    )
+    expected_graph = '<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria"@en .'
+
+    run_test(input_graph, kgcl_patch, expected_graph)
diff --git a/kgcl/tests/test_apply/test_create_edge.py b/kgcl/tests/test_apply/test_create_edge.py
new file mode 100644
index 00000000..70dcb687
--- /dev/null
+++ b/kgcl/tests/test_apply/test_create_edge.py
@@ -0,0 +1,66 @@
+from kgcl.tests.test_apply.util import run_test
+
+
+def test_create_edge_with_ids():
+    input_graph = ""
+    kgcl_patch = "create edge <http://example.org/subject> <http://example.org/predicate> <http://example.org/object>"
+    expected_graph = """_:Nfb1ff91c88fe4e328efb4ceea0a11730 <http://www.w3.org/2002/07/owl#onProperty> <http://example.org/predicate> .
+                        _:Nfb1ff91c88fe4e328efb4ceea0a11730 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Restriction> .
+                        <http://example.org/subject> <http://www.w3.org/2000/01/rdf-schema#subClassOf> _:Nfb1ff91c88fe4e328efb4ceea0a11730 .
+                        _:Nfb1ff91c88fe4e328efb4ceea0a11730 <http://www.w3.org/2002/07/owl#someValuesFrom> <http://example.org/object> ."""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_create_edge_with_curies():
+    input_graph = ""
+    kgcl_patch = "create edge ex:subject ex:predicate ex:object"
+    expected_graph = """_:Nfb1ff91c88fe4e328efb4ceea0a11730 <http://www.w3.org/2002/07/owl#onProperty> <http://example.org/predicate> .
+                        _:Nfb1ff91c88fe4e328efb4ceea0a11730 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Restriction> .
+                        <http://example.org/subject> <http://www.w3.org/2000/01/rdf-schema#subClassOf> _:Nfb1ff91c88fe4e328efb4ceea0a11730 .
+                        _:Nfb1ff91c88fe4e328efb4ceea0a11730 <http://www.w3.org/2002/07/owl#someValuesFrom> <http://example.org/object> ."""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_create_edge_with_labels():
+    input_graph = """<http://example.org/subject> <http://www.w3.org/2000/01/rdf-schema#label> "subject" .
+                     <http://example.org/predicate> <http://www.w3.org/2000/01/rdf-schema#label> "predicate" .
+                     <http://example.org/object> <http://www.w3.org/2000/01/rdf-schema#label> "object" ."""
+    kgcl_patch = "create edge 'subject' 'predicate' 'object'"
+    expected_graph = """_:Nfb1ff91c88fe4e328efb4ceea0a11730 <http://www.w3.org/2002/07/owl#onProperty> <http://example.org/predicate> .
+                        _:Nfb1ff91c88fe4e328efb4ceea0a11730 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Restriction> .
+                        <http://example.org/subject> <http://www.w3.org/2000/01/rdf-schema#subClassOf> _:Nfb1ff91c88fe4e328efb4ceea0a11730 .
+                        _:Nfb1ff91c88fe4e328efb4ceea0a11730 <http://www.w3.org/2002/07/owl#someValuesFrom> <http://example.org/object> .
+                        <http://example.org/subject> <http://www.w3.org/2000/01/rdf-schema#label> "subject" .
+                        <http://example.org/predicate> <http://www.w3.org/2000/01/rdf-schema#label> "predicate" .
+                        <http://example.org/object> <http://www.w3.org/2000/01/rdf-schema#label> "object" ."""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_create_subsumption_with_ids():
+    input_graph = ""
+    kgcl_patch = "create edge <http://example.org/subclass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/superclass>"
+    expected_graph = "<http://example.org/subclass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/superclass> ."
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_create_subsumption_with_curies():
+    input_graph = ""
+    kgcl_patch = "create edge ex:subclass rdfs:subClassOf ex:superclass"
+    expected_graph = "<http://example.org/subclass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/superclass> ."
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_create_subsumption_with_labels():
+    input_graph = """<http://example.org/subclass> <http://www.w3.org/2000/01/rdf-schema#label> "subclass" .
+                     <http://example.org/superclass> <http://www.w3.org/2000/01/rdf-schema#label> "superclass" ."""
+    kgcl_patch = "create edge 'subclass' rdfs:subClassOf 'superclass'"
+    expected_graph = """<http://example.org/subclass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/superclass> .
+                        <http://example.org/subclass> <http://www.w3.org/2000/01/rdf-schema#label> "subclass" .
+                        <http://example.org/superclass> <http://www.w3.org/2000/01/rdf-schema#label> "superclass" ."""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
diff --git a/kgcl/tests/test_apply/test_create_synonym.py b/kgcl/tests/test_apply/test_create_synonym.py
new file mode 100644
index 00000000..fd92f5ee
--- /dev/null
+++ b/kgcl/tests/test_apply/test_create_synonym.py
@@ -0,0 +1,92 @@
+from kgcl.tests.test_apply.util import run_test
+
+
+def test_create_synonym_with_ids():
+    input_graph = '<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria" .'
+    kgcl_patch = (
+        "create synonym 'Virus' for <http://purl.obolibrary.org/obo/NCBITaxon_2>"
+    )
+    expected_graph = """<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria" .
+                        <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.geneontology.org/formats/oboInOwl#hasSynonym> "Virus" ."""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_create_synonym_with_language_tag():
+    input_graph = '<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria" .'
+    kgcl_patch = (
+        "create synonym 'Virus'@en for <http://purl.obolibrary.org/obo/NCBITaxon_2>"
+    )
+    expected_graph = """<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria" .
+                        <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.geneontology.org/formats/oboInOwl#hasSynonym> "Virus"@en ."""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_create_broad_synonym_with_ids():
+    input_graph = '<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria" .'
+    kgcl_patch = (
+        "create broad synonym 'Virus' for <http://purl.obolibrary.org/obo/NCBITaxon_2>"
+    )
+    expected_graph = """<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria" .  
+                        <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.geneontology.org/formats/oboInOwl#hasBroadSynonym> "Virus" ."""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_create_broad_synonym_with_curies():
+    input_graph = '<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria" .'
+    kgcl_patch = "create broad synonym 'Virus' for obo:NCBITaxon_2"
+    expected_graph = """<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria" .  
+                        <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.geneontology.org/formats/oboInOwl#hasBroadSynonym> "Virus" ."""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_create_broad_synonym_with_labels():
+    input_graph = '<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria" .'
+    kgcl_patch = "create broad synonym 'Virus' for 'Bacteria'"
+    expected_graph = """<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria" .  
+                        <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.geneontology.org/formats/oboInOwl#hasBroadSynonym> "Virus" ."""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_create_exact_synonym_with_ids():
+    input_graph = '<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria" .'
+    kgcl_patch = (
+        "create exact synonym 'Virus' for <http://purl.obolibrary.org/obo/NCBITaxon_2>"
+    )
+    expected_graph = """<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria" .
+                        <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.geneontology.org/formats/oboInOwl#hasExactSynonym> "Virus" ."""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_create_narrow_synonym_with_ids():
+    input_graph = '<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria" .'
+    kgcl_patch = (
+        "create narrow synonym 'Virus' for <http://purl.obolibrary.org/obo/NCBITaxon_2>"
+    )
+    expected_graph = """<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria" .
+                        <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.geneontology.org/formats/oboInOwl#hasNarrowSynonym> "Virus" ."""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_create_related_synonym_with_ids():
+    input_graph = '<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria" .'
+    kgcl_patch = "create related synonym 'Virus' for <http://purl.obolibrary.org/obo/NCBITaxon_2>"
+    expected_graph = """<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria" .
+                        <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.geneontology.org/formats/oboInOwl#hasRelatedSynonym> "Virus" ."""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_create_related_synonym_with_language_tags_and_labels():
+    input_graph = '<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria" .'
+    kgcl_patch = "create related synonym 'Virus'@en for 'Bacteria'"
+    expected_graph = """<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria" .
+                        <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.geneontology.org/formats/oboInOwl#hasRelatedSynonym> "Virus"@en ."""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
diff --git a/kgcl/tests/test_apply/test_delete_edge.py b/kgcl/tests/test_apply/test_delete_edge.py
new file mode 100644
index 00000000..5b4504cd
--- /dev/null
+++ b/kgcl/tests/test_apply/test_delete_edge.py
@@ -0,0 +1,55 @@
+from kgcl.tests.test_apply.util import run_test
+
+
+def test_delete_edge_with_ids():
+    input_graph = """_:Ndb451c3aad3841c88d7042f29b20bea8 <http://www.w3.org/2002/07/owl#onProperty> <http://example.org/predicate> .
+                     _:Ndb451c3aad3841c88d7042f29b20bea8 <http://www.w3.org/2002/07/owl#someValuesFrom> <http://example.org/object> .
+                     _:Ndb451c3aad3841c88d7042f29b20bea8 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Restriction> .
+                     <http://example.org/subject> <http://www.w3.org/2000/01/rdf-schema#subClassOf> _:Ndb451c3aad3841c88d7042f29b20bea8 ."""
+    kgcl_patch = "delete edge <http://example.org/subject> <http://example.org/predicate> <http://example.org/object>"
+    expected_graph = ""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_delete_edge_with_curies():
+    input_graph = """_:Ndb451c3aad3841c88d7042f29b20bea8 <http://www.w3.org/2002/07/owl#onProperty> <http://example.org/predicate> .
+                     _:Ndb451c3aad3841c88d7042f29b20bea8 <http://www.w3.org/2002/07/owl#someValuesFrom> <http://example.org/object> .
+                     _:Ndb451c3aad3841c88d7042f29b20bea8 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Restriction> .
+                     <http://example.org/subject> <http://www.w3.org/2000/01/rdf-schema#subClassOf> _:Ndb451c3aad3841c88d7042f29b20bea8 ."""
+    kgcl_patch = "delete edge ex:subject ex:predicate ex:object"
+    expected_graph = ""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_delete_edge_with_labels():
+    input_graph = """_:Ndb451c3aad3841c88d7042f29b20bea8 <http://www.w3.org/2002/07/owl#onProperty> <http://example.org/predicate> .
+                     _:Ndb451c3aad3841c88d7042f29b20bea8 <http://www.w3.org/2002/07/owl#someValuesFrom> <http://example.org/object> .
+                     _:Ndb451c3aad3841c88d7042f29b20bea8 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Restriction> .
+                     <http://example.org/subject> <http://www.w3.org/2000/01/rdf-schema#subClassOf> _:Ndb451c3aad3841c88d7042f29b20bea8 .
+                     <http://example.org/subject> <http://www.w3.org/2000/01/rdf-schema#label> "subject" .
+                     <http://example.org/predicate> <http://www.w3.org/2000/01/rdf-schema#label> "predicate" .
+                     <http://example.org/object> <http://www.w3.org/2000/01/rdf-schema#label> "object" ."""
+    kgcl_patch = "delete edge 'subject' 'predicate' 'object'"
+    expected_graph = """<http://example.org/subject> <http://www.w3.org/2000/01/rdf-schema#label> "subject" .
+                        <http://example.org/predicate> <http://www.w3.org/2000/01/rdf-schema#label> "predicate" .
+                        <http://example.org/object> <http://www.w3.org/2000/01/rdf-schema#label> "object" ."""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_delete_subsumption_with_ids():
+    input_graph = "<http://example.org/subclass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/superclass> ."
+    kgcl_patch = "delete edge <http://example.org/subclass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/superclass>"
+    expected_graph = ""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_delete_subsumption_with_curies():
+    input_graph = "<http://example.org/subclass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/superclass> ."
+    kgcl_patch = "delete edge ex:subclass rdfs:subClassOf ex:superclass"
+    expected_graph = ""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
diff --git a/kgcl/tests/test_apply/test_delete_entity.py b/kgcl/tests/test_apply/test_delete_entity.py
new file mode 100644
index 00000000..cb91fd73
--- /dev/null
+++ b/kgcl/tests/test_apply/test_delete_entity.py
@@ -0,0 +1,26 @@
+from kgcl.tests.test_apply.util import run_test
+
+
+def test_delete_annotated_edge_with_ids():
+    input_graph = "<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Class> ."
+    kgcl_patch = "delete <http://purl.obolibrary.org/obo/NCBITaxon_2>"
+    expected_graph = ""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_delete_annotated_edge_with_curies():
+    input_graph = "<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Class> ."
+    kgcl_patch = "delete obo:NCBITaxon_2"
+    expected_graph = ""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_delete_annotated_edge_with_label():
+    input_graph = """<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Class> .  
+                     <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria" ."""
+    kgcl_patch = "delete 'Bacteria'"
+    expected_graph = ""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
diff --git a/kgcl/tests/test_apply/test_node_deepening.py b/kgcl/tests/test_apply/test_node_deepening.py
new file mode 100644
index 00000000..8b587346
--- /dev/null
+++ b/kgcl/tests/test_apply/test_node_deepening.py
@@ -0,0 +1,37 @@
+from kgcl.tests.test_apply.util import run_test
+
+
+def test_node_deepening_with_ids():
+    input_graph = """<http://example.org/targetClass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/superclass> .
+                   <http://example.org/subclass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/superclass> ."""
+    kgcl_patch = "deepen <http://example.org/targetClass> from <http://example.org/superclass> to <http://example.org/subclass>"
+    expected_graph = """<http://example.org/subclass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/superclass> .
+                        <http://example.org/targetClass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/subclass> ."""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_node_deepening_with_curies():
+    input_graph = """<http://example.org/targetClass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/superclass> .
+                   <http://example.org/subclass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/superclass> ."""
+    kgcl_patch = "deepen ex:targetClass from ex:superclass to ex:subclass"
+    expected_graph = """<http://example.org/subclass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/superclass> .
+                        <http://example.org/targetClass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/subclass> ."""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_node_deepening_with_labels():
+    input_graph = """<http://example.org/targetClass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/superclass> .
+                     <http://example.org/subclass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/superclass> .
+                     <http://example.org/targetClass> <http://www.w3.org/2000/01/rdf-schema#label> "targetClass" .
+                     <http://example.org/superclass> <http://www.w3.org/2000/01/rdf-schema#label> "superclass" .
+                     <http://example.org/subclass> <http://www.w3.org/2000/01/rdf-schema#label> "subclass" ."""
+    kgcl_patch = "deepen 'targetClass' from 'superclass' to 'subclass'"
+    expected_graph = """<http://example.org/subclass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/superclass> .
+                        <http://example.org/targetClass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/subclass> .
+                        <http://example.org/targetClass> <http://www.w3.org/2000/01/rdf-schema#label> "targetClass" .
+                        <http://example.org/superclass> <http://www.w3.org/2000/01/rdf-schema#label> "superclass" .
+                        <http://example.org/subclass> <http://www.w3.org/2000/01/rdf-schema#label> "subclass" ."""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
diff --git a/kgcl/tests/test_apply/test_node_move.py b/kgcl/tests/test_apply/test_node_move.py
new file mode 100644
index 00000000..6379c438
--- /dev/null
+++ b/kgcl/tests/test_apply/test_node_move.py
@@ -0,0 +1,31 @@
+from kgcl.tests.test_apply.util import run_test
+
+
+def test_node_move_with_ids():
+    input_graph = "<http://example.org/targetClass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/A> ."
+    kgcl_patch = "move <http://example.org/targetClass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/A> from <http://example.org/A> to <http://example.org/B>"
+    expected_graph = "<http://example.org/targetClass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/B> ."
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_node_move_with_curies():
+    input_graph = "<http://example.org/targetClass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/A> ."
+    kgcl_patch = "move ex:targetClass <http://www.w3.org/2000/01/rdf-schema#subClassOf> ex:A from ex:A to ex:B"
+    expected_graph = "<http://example.org/targetClass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/B> ."
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_node_move_with_labels():
+    input_graph = """<http://example.org/targetClass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/A> .  
+                     <http://example.org/targetClass> <http://www.w3.org/2000/01/rdf-schema#label> "targetClass" .
+                     <http://example.org/A> <http://www.w3.org/2000/01/rdf-schema#label> "A" .
+                     <http://example.org/B> <http://www.w3.org/2000/01/rdf-schema#label> "B" ."""
+    kgcl_patch = "move 'targetClass' <http://www.w3.org/2000/01/rdf-schema#subClassOf> 'A' from 'A' to 'B'"
+    expected_graph = """<http://example.org/targetClass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/B> .
+                        <http://example.org/targetClass> <http://www.w3.org/2000/01/rdf-schema#label> "targetClass" .
+                        <http://example.org/A> <http://www.w3.org/2000/01/rdf-schema#label> "A" .
+                        <http://example.org/B> <http://www.w3.org/2000/01/rdf-schema#label> "B" ."""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
diff --git a/kgcl/tests/test_apply/test_node_shallowing.py b/kgcl/tests/test_apply/test_node_shallowing.py
new file mode 100644
index 00000000..a9dce425
--- /dev/null
+++ b/kgcl/tests/test_apply/test_node_shallowing.py
@@ -0,0 +1,38 @@
+from kgcl.tests.test_apply.util import run_test
+
+
+def test_node_shallowing_with_ids():
+    input_graph = """<http://example.org/targetClass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/subclass> .
+                     <http://example.org/subclass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/superclass> ."""
+    kgcl_patch = "shallow <http://example.org/targetClass> from <http://example.org/subclass> to <http://example.org/superclass>"
+    expected_graph = """<http://example.org/targetClass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/superclass> .
+                        <http://example.org/subclass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/superclass> ."""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_node_shallowing_with_curies():
+    input_graph = """<http://example.org/targetClass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/subclass> .
+                     <http://example.org/subclass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/superclass> ."""
+    kgcl_patch = "shallow ex:targetClass from ex:subclass to ex:superclass"
+    expected_graph = """<http://example.org/targetClass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/superclass> .
+                        <http://example.org/subclass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/superclass> ."""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_node_shallowing_with_labels():
+    input_graph = """<http://example.org/targetClass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/subclass> .
+                     <http://example.org/subclass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/superclass> .  
+                     <http://example.org/targetClass> <http://www.w3.org/2000/01/rdf-schema#label> "targetClass" .
+                     <http://example.org/subclass> <http://www.w3.org/2000/01/rdf-schema#label> "subclass" .
+                     <http://example.org/superclass> <http://www.w3.org/2000/01/rdf-schema#label> "superclass" .
+                     """
+    kgcl_patch = "shallow 'targetClass' from 'subclass' to 'superclass'"
+    expected_graph = """<http://example.org/targetClass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/superclass> .
+                        <http://example.org/subclass> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://example.org/superclass> .
+                        <http://example.org/targetClass> <http://www.w3.org/2000/01/rdf-schema#label> "targetClass" .
+                        <http://example.org/subclass> <http://www.w3.org/2000/01/rdf-schema#label> "subclass" .
+                        <http://example.org/superclass> <http://www.w3.org/2000/01/rdf-schema#label> "superclass" ."""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
diff --git a/kgcl/tests/test_apply/test_obsoletion.py b/kgcl/tests/test_apply/test_obsoletion.py
new file mode 100644
index 00000000..4cdfb603
--- /dev/null
+++ b/kgcl/tests/test_apply/test_obsoletion.py
@@ -0,0 +1,37 @@
+from kgcl.tests.test_apply.util import run_test
+
+
+def test_obsoletion_with_ids():
+    input_graph = """<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria" .
+                     <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Class> ."""
+    kgcl_patch = "obsolete <http://purl.obolibrary.org/obo/NCBITaxon_2>"
+    expected_graph = """<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "obsolete Bacteria" .
+                       <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2002/07/owl#deprecated> "true"^^<http://www.w3.org/2001/XMLSchema#boolean> .
+                       <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Class> .
+                       <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://www.geneontology.org/formats/oboInOwl#ObsoleteClass> ."""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_obsoletion_with_curies():
+    input_graph = """<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria" .
+                     <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Class> ."""
+    kgcl_patch = "obsolete obo:NCBITaxon_2"
+    expected_graph = """<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "obsolete Bacteria" .
+                       <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2002/07/owl#deprecated> "true"^^<http://www.w3.org/2001/XMLSchema#boolean> .
+                       <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Class> .
+                       <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://www.geneontology.org/formats/oboInOwl#ObsoleteClass> ."""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_obsoletion_with_labes():
+    input_graph = """<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria" .
+                     <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Class> ."""
+    kgcl_patch = "obsolete 'Bacteria'"
+    expected_graph = """<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "obsolete Bacteria" .
+                       <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2002/07/owl#deprecated> "true"^^<http://www.w3.org/2001/XMLSchema#boolean> .
+                       <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Class> .
+                       <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://www.geneontology.org/formats/oboInOwl#ObsoleteClass> ."""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
diff --git a/kgcl/tests/test_apply/test_rename.py b/kgcl/tests/test_apply/test_rename.py
new file mode 100644
index 00000000..85e270c9
--- /dev/null
+++ b/kgcl/tests/test_apply/test_rename.py
@@ -0,0 +1,29 @@
+from kgcl.tests.test_apply.util import run_test
+
+
+def test_rename():
+    input_graph = '<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria" .'
+    kgcl_patch = "rename 'Bacteria' to 'Virus'"
+    expected_graph = '<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Virus" .'
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_test_rename_with_language_tag():
+    input_graph = '<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria"@en .'
+    kgcl_patch = "rename 'Bacteria'@en to 'Virus'@fr"
+    expected_graph = '<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Virus"@fr .'
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_rename_with_id():
+    input_graph = """<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria" .
+                    <http://purl.obolibrary.org/obo/NCBITaxon_8> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria" ."""
+    kgcl_patch = (
+        "rename <http://purl.obolibrary.org/obo/NCBITaxon_2> from 'Bacteria' to 'Virus'"
+    )
+    expected_graph = """<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Virus" .
+                        <http://purl.obolibrary.org/obo/NCBITaxon_8> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria" ."""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
diff --git a/kgcl/tests/test_apply/test_unobsolete.py b/kgcl/tests/test_apply/test_unobsolete.py
new file mode 100644
index 00000000..f75f7ba6
--- /dev/null
+++ b/kgcl/tests/test_apply/test_unobsolete.py
@@ -0,0 +1,34 @@
+from kgcl.tests.test_apply.util import run_test
+
+
+def test_unobsolete_with_id():
+    input_graph = """<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2002/07/owl#deprecated> "true"^^<http://www.w3.org/2001/XMLSchema#boolean> .
+                    <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Class> .
+                    <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "obsolete Bacteria" ."""
+    kgcl_patch = "unobsolete <http://purl.obolibrary.org/obo/NCBITaxon_2>"
+    expected_graph = """<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria" .
+                        <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Class> ."""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_unobsolete_with_curies():
+    input_graph = """<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2002/07/owl#deprecated> "true"^^<http://www.w3.org/2001/XMLSchema#boolean> .
+                    <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Class> .
+                    <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "obsolete Bacteria" ."""
+    kgcl_patch = "unobsolete obo:NCBITaxon_2"
+    expected_graph = """<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria" .
+                        <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Class> ."""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
+
+
+def test_unobsolete_with_label():
+    input_graph = """<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2002/07/owl#deprecated> "true"^^<http://www.w3.org/2001/XMLSchema#boolean> .
+                    <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Class> .
+                    <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "obsolete Bacteria" ."""
+    kgcl_patch = "unobsolete 'obsolete Bacteria'"
+    expected_graph = """<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Bacteria" .
+                        <http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Class> ."""
+
+    run_test(input_graph, kgcl_patch, expected_graph)
diff --git a/kgcl/tests/test_apply/util.py b/kgcl/tests/test_apply/util.py
new file mode 100644
index 00000000..565039cb
--- /dev/null
+++ b/kgcl/tests/test_apply/util.py
@@ -0,0 +1,34 @@
+from rdflib.compare import to_isomorphic, graph_diff
+from rdflib import URIRef, Literal, Graph
+from kgcl.apply.graph_transformer import apply_patch
+from kgcl.grammar.parser import parse
+
+
+def compare_graphs(actual, expected):
+    actual_iso = to_isomorphic(actual)
+    expected_iso = to_isomorphic(expected)
+
+    if actual_iso != expected_iso:
+        _, in_first, in_second = graph_diff(actual_iso, expected_iso)
+        print("The actual and expected graphs differ")
+        print("----- Contents of actual graph not in expected graph -----")
+        print(in_first.serialize(format="nt").decode("utf-8"))
+        # dump_ttl_sorted(in_first)
+        print("----- Contents of expected graph not in actual graph -----")
+        print(in_second.serialize(format="nt").decode("utf-8"))
+        # dump_ttl_sorted(in_second)
+
+    assert actual_iso == expected_iso
+
+
+def run_test(input_graph, kgcl_patch, expected_graph):
+    # load input graph
+    g = Graph().parse(data=input_graph, format="nt")
+
+    # load expected output graph
+    expected = Graph().parse(data=expected_graph, format="nt")
+
+    parsed_patch = parse(kgcl_patch)
+    apply_patch(parsed_patch, g)
+
+    compare_graphs(g, expected)
diff --git a/kgcl/tests/test_model.py b/kgcl/tests/test_model.py
new file mode 100644
index 00000000..e69de29b
diff --git a/parser/test/kgcl.lark b/parser/test/kgcl.lark
deleted file mode 100644
index 521746e3..00000000
--- a/parser/test/kgcl.lark
+++ /dev/null
@@ -1,10 +0,0 @@
-
-?expression: rename
-
-rename : "rename" STRING "to" STRING
-
-STRING: /\S+/
-
-%import common.WS
-%ignore WS
-
diff --git a/parser/test/parser.py b/parser/test/parser.py
deleted file mode 100644
index a2a86af4..00000000
--- a/parser/test/parser.py
+++ /dev/null
@@ -1,38 +0,0 @@
-from lark import Lark, Tree, Token
-
-
-def idGenerator():
-    id = 0
-    while True:
-        yield id
-        id += 1
-
-kgcl_parser = Lark.open('kgcl.lark', start='expression')
-
-def getTokens(tree):
-    return tree.scan_values(lambda v: isinstance(v, Token))
-
-
-
-if __name__ == "__main__":
-    ###
-    ### MANUAL TESTING
-    ###
-#TODO: cannot work out how to parse strings without delimiter
-    example2 = "rename abnormal to moreg"
-    tree2 = kgcl_parser.parse(example2); 
-    print(tree2)
-
-    #test = tree.find_data('old')
-    #old = next(test)
-    #old_token = next(getTokens(old))
-    #print(old_token)
-
-
-    #example2 = "obsolete 'abnormal ear'"
-    #tree2 = kgcl_parser.parse(example2); 
-    #obsolete_token = next(getTokens(tree2))
-    #print(tree2)
-    #print(obsolete_token)
-
-
diff --git a/parser/testData/example/edgeCreation/kgcl b/parser/testData/example/edgeCreation/kgcl
deleted file mode 100644
index 69791623..00000000
--- a/parser/testData/example/edgeCreation/kgcl
+++ /dev/null
@@ -1,1 +0,0 @@
-create edge <http://example.org/predicate> from <http://example.org/subject> to <http://example.org/object>
diff --git a/parser/testData/example/edgeDeletion/graph.nt b/parser/testData/example/edgeDeletion/graph.nt
deleted file mode 100644
index 0529c785..00000000
--- a/parser/testData/example/edgeDeletion/graph.nt
+++ /dev/null
@@ -1,1 +0,0 @@
-<http://example.org/subject> <http://example.org/predicate> <http://example.org/object> .
diff --git a/parser/testData/example/edgeDeletion/kgcl b/parser/testData/example/edgeDeletion/kgcl
deleted file mode 100644
index 7b39ae7f..00000000
--- a/parser/testData/example/edgeDeletion/kgcl
+++ /dev/null
@@ -1,1 +0,0 @@
-delete edge <http://example.org/predicate> from <http://example.org/subject> to <http://example.org/object>
diff --git a/parser/testData/example/move/kgcl b/parser/testData/example/move/kgcl
deleted file mode 100644
index 66b02892..00000000
--- a/parser/testData/example/move/kgcl
+++ /dev/null
@@ -1,1 +0,0 @@
-move <http://example.org/targetClass> from <http://example.org/A> to <http://example.org/B>
diff --git a/parser/testData/example/nodeCreation/kgcl b/parser/testData/example/nodeCreation/kgcl
deleted file mode 100644
index a7bc1941..00000000
--- a/parser/testData/example/nodeCreation/kgcl
+++ /dev/null
@@ -1,2 +0,0 @@
-create node <http://purl.obolibrary.org/obo/NCBITaxon_2> 'Bacteria'
-create <http://purl.obolibrary.org/obo/NCBITaxon_4>
diff --git a/parser/testData/example/nodeDeepening/kgcl b/parser/testData/example/nodeDeepening/kgcl
deleted file mode 100644
index c9f4470d..00000000
--- a/parser/testData/example/nodeDeepening/kgcl
+++ /dev/null
@@ -1,1 +0,0 @@
-deepen <http://example.org/targetClass> from <http://example.org/superclass> to <http://example.org/subclass>
diff --git a/parser/testData/example/nodeShallowing/kgcl b/parser/testData/example/nodeShallowing/kgcl
deleted file mode 100644
index 3aa9b2b0..00000000
--- a/parser/testData/example/nodeShallowing/kgcl
+++ /dev/null
@@ -1,1 +0,0 @@
-shallow <http://example.org/targetClass> from <http://example.org/subclass> to <http://example.org/superclass>
diff --git a/parser/testData/example/obsoleteById/kgcl b/parser/testData/example/obsoleteById/kgcl
deleted file mode 100644
index 32c56ab2..00000000
--- a/parser/testData/example/obsoleteById/kgcl
+++ /dev/null
@@ -1,1 +0,0 @@
-obsolete <http://purl.obolibrary.org/obo/NCBITaxon_2>
diff --git a/parser/testData/example/unobsoleteById/kgcl b/parser/testData/example/unobsoleteById/kgcl
deleted file mode 100644
index 1d5c17f3..00000000
--- a/parser/testData/example/unobsoleteById/kgcl
+++ /dev/null
@@ -1,1 +0,0 @@
-unobsolete <http://purl.obolibrary.org/obo/NCBITaxon_2>
diff --git a/parser/testData/kgcl b/parser/testData/kgcl
deleted file mode 100644
index 5bf54e04..00000000
--- a/parser/testData/kgcl
+++ /dev/null
@@ -1,2 +0,0 @@
-rename 'Bacteria' to 'Virus'
-rename 'Virus' to 'Vaccine'
diff --git a/parser/testData/transformation.nt b/parser/testData/transformation.nt
deleted file mode 100644
index 0bccccfa..00000000
--- a/parser/testData/transformation.nt
+++ /dev/null
@@ -1,2 +0,0 @@
-<http://purl.obolibrary.org/obo/NCBITaxon_2> <http://www.w3.org/2000/01/rdf-schema#label> "Vaccine" .
-
diff --git a/parser/test_graph_transformer.py b/parser/test_graph_transformer.py
deleted file mode 100644
index 39ed9238..00000000
--- a/parser/test_graph_transformer.py
+++ /dev/null
@@ -1,343 +0,0 @@
-import unittest
-from rdflib.namespace import CSVW, DC, DCAT, DCTERMS, DOAP, FOAF, ODRL2, ORG, OWL, \
-                           PROF, PROV, RDF, RDFS, SDO, SH, SKOS, SOSA, SSN, TIME, \
-                           VOID, XMLNS, XSD 
-from rdflib import URIRef, BNode, Literal 
-from rdflib.compare import to_isomorphic, graph_diff
-
-import rdflib
-import parser
-import kgcl_2_sparql
-import graph_transformer
-
-def compare_graphs(actual, expected):
-     actual_iso = to_isomorphic(actual)
-     expected_iso = to_isomorphic(expected)
- 
-     if actual_iso != expected_iso: 
-         _, in_first, in_second = graph_diff(actual_iso, expected_iso)
-         print("The actual and expected graphs differ")
-         print("----- Contents of actual graph not in expected graph -----") 
-         print(in_first.serialize(format="nt").decode("utf-8")) 
-         #dump_ttl_sorted(in_first)
-         print("----- Contents of expected graph not in actual graph -----")
-         print(in_second.serialize(format="nt").decode("utf-8")) 
-         #dump_ttl_sorted(in_second)     
-
-     assert actual_iso == expected_iso
-
-class TestRenaming(unittest.TestCase):
-
-    def test_command(self): 
-        #KGCL statement
-        command = "rename 'Bacteria' to 'RenamedBacteria'"
-
-        #create test graph (containing only one triple)
-        g = rdflib.Graph() 
-        taxon = URIRef("http://purl.obolibrary.org/obo/NCBITaxon_2") 
-        bacteriaLabel = Literal('Bacteria')
-        g.add((taxon, RDFS.label, bacteriaLabel))
-
-        expectedGraph = rdflib.Graph() 
-        #taxon = URIRef("http://purl.obolibrary.org/obo/NCBITaxon_2") 
-        renamedBacteriaLabel = Literal('RenamedBacteria')
-        expectedGraph.add((taxon, RDFS.label, renamedBacteriaLabel)) 
-
-        #parse KGCL statement into data model
-        kgclModel = parser.parse(command)
-
-        graph_transformer.transform_graph(kgclModel, g)
-
-        compare_graphs(g,expectedGraph)
-
-class TestNodeCreation(unittest.TestCase):
-
-    def test_command_with_label(self): 
-        #KGCL statement
-        command = "create node <http://purl.obolibrary.org/obo/NCBITaxon_2> 'Bacteria'"
-
-        #create test graph (containing only one triple)
-        g = rdflib.Graph() 
-
-        expectedGraph = rdflib.Graph() 
-        taxon = URIRef("http://purl.obolibrary.org/obo/NCBITaxon_2") 
-        label = Literal('Bacteria')
-        expectedGraph.add((taxon, RDFS.label, label)) 
-
-        #parse KGCL statement into data model
-        kgclModel = parser.parse(command)
-
-        graph_transformer.transform_graph(kgclModel, g)
-
-        compare_graphs(g,expectedGraph) 
-
-    def test_command_without_label(self): 
-        #KGCL statement
-        command = "create <http://purl.obolibrary.org/obo/NCBITaxon_2>"
-
-        #create test graph (containing only one triple)
-        g = rdflib.Graph() 
-
-        expectedGraph = rdflib.Graph() 
-        taxon = URIRef("http://purl.obolibrary.org/obo/NCBITaxon_2") 
-        expectedGraph.add((taxon, RDF.type, OWL.Class)) 
-
-        #parse KGCL statement into data model
-        kgclModel = parser.parse(command)
-
-        graph_transformer.transform_graph(kgclModel, g)
-
-        compare_graphs(g,expectedGraph) 
-
-class TestNodeDeletion(unittest.TestCase):
-
-    def test_command_with_label(self): 
-        #KGCL statement
-        command = "delete 'Bacteria'"
-
-        #create test graph (containing only one triple)
-        g = rdflib.Graph() 
-        taxon = URIRef("http://purl.obolibrary.org/obo/NCBITaxon_2") 
-        obiClass = URIRef("http://purl.obolibrary.org/obo/OBI_0100026") 
-        label = Literal('Bacteria')
-        g.add((taxon, RDFS.label, label)) 
-        g.add((taxon, RDF.type, OWL.Class)) 
-        g.add((obiClass, RDF.type, OWL.Class)) 
-        g.add((taxon, RDFS.subClassOf, obiClass))
-
-        expectedGraph = rdflib.Graph() 
-        expectedGraph.add((obiClass, RDF.type, OWL.Class)) 
-
-        #parse KGCL statement into data model
-        kgclModel = parser.parse(command)
-
-        graph_transformer.transform_graph(kgclModel, g)
-
-        compare_graphs(g,expectedGraph) 
-
-
-    def test_command_with_id(self): 
-        #KGCL statement
-        command = "delete <http://purl.obolibrary.org/obo/NCBITaxon_2>"
-
-        #create test graph (containing only one triple)
-        g = rdflib.Graph() 
-        taxon = URIRef("http://purl.obolibrary.org/obo/NCBITaxon_2") 
-        obiClass = URIRef("http://purl.obolibrary.org/obo/OBI_0100026") 
-        label = Literal('Bacteria')
-        g.add((taxon, RDFS.label, label)) 
-        g.add((taxon, RDF.type, OWL.Class)) 
-        g.add((obiClass, RDF.type, OWL.Class)) 
-        g.add((taxon, RDFS.subClassOf, obiClass))
-
-        expectedGraph = rdflib.Graph() 
-        expectedGraph.add((obiClass, RDF.type, OWL.Class)) 
-
-        #parse KGCL statement into data model
-        kgclModel = parser.parse(command)
-
-        graph_transformer.transform_graph(kgclModel, g)
-
-        compare_graphs(g,expectedGraph) 
-
-class TestNodeObsoletion(unittest.TestCase):
-
-    def test_command_with_label(self): 
-        #KGCL statement
-        command = "obsolete 'Bacteria'"
-
-        #create test graph (containing only one triple)
-        g = rdflib.Graph() 
-        taxon = URIRef("http://purl.obolibrary.org/obo/NCBITaxon_2") 
-        label = Literal('Bacteria')
-        g.add((taxon, RDFS.label, label)) 
-        g.add((taxon, RDF.type, OWL.Class)) 
-
-        expectedGraph = rdflib.Graph() 
-        obsoleteLabel = Literal('obsolete Bacteria') 
-        true_bool = rdflib.Literal("true", datatype=XSD.boolean) 
-        #trueLiteral = Literal('\"true\"^^xsd:boolean') 
-        expectedGraph.add((taxon, RDFS.label,obsoleteLabel)) 
-        expectedGraph.add((taxon, RDF.type, OWL.Class)) 
-        expectedGraph.add((taxon, OWL.deprecated, true_bool)) 
-
-        #parse KGCL statement into data model
-        kgclModel = parser.parse(command)
-
-        graph_transformer.transform_graph(kgclModel, g)
-
-        compare_graphs(g,expectedGraph) 
-
-
-    def test_command_with_id(self): 
-        #KGCL statement
-        command = "obsolete <http://purl.obolibrary.org/obo/NCBITaxon_2>"
-
-        #create test graph (containing only one triple)
-        g = rdflib.Graph() 
-        taxon = URIRef("http://purl.obolibrary.org/obo/NCBITaxon_2") 
-        label = Literal('Bacteria')
-        g.add((taxon, RDFS.label, label)) 
-        g.add((taxon, RDF.type, OWL.Class)) 
-
-        expectedGraph = rdflib.Graph() 
-        obsoleteLabel = Literal('obsolete Bacteria') 
-        true_bool = rdflib.Literal("true", datatype=XSD.boolean) 
-        #trueLiteral = Literal('\"true\"^^xsd:boolean') 
-        expectedGraph.add((taxon, RDFS.label,obsoleteLabel)) 
-        expectedGraph.add((taxon, RDF.type, OWL.Class)) 
-        expectedGraph.add((taxon, OWL.deprecated, true_bool)) 
-
-        #parse KGCL statement into data model
-        kgclModel = parser.parse(command)
-
-        graph_transformer.transform_graph(kgclModel, g)
-
-        compare_graphs(g,expectedGraph) 
-
-class TestNodeUnobsoletion(unittest.TestCase): 
-    def test_command_with_id(self): 
-        #KGCL statement
-        command = "unobsolete <http://purl.obolibrary.org/obo/NCBITaxon_2>"
-
-        #create test graph (containing only one triple) 
-        g = rdflib.Graph() 
-        taxon = URIRef("http://purl.obolibrary.org/obo/NCBITaxon_2") 
-        obsoleteLabel = Literal('obsolete Bacteria') 
-        true_bool = rdflib.Literal("true", datatype=XSD.boolean) 
-        g.add((taxon, RDFS.label, obsoleteLabel)) 
-        g.add((taxon, RDF.type, OWL.Class)) 
-        g.add((taxon, OWL.deprecated, true_bool)) 
-
-        expectedGraph = rdflib.Graph() 
-        label = Literal('Bacteria') 
-        expectedGraph.add((taxon, RDFS.label,label)) 
-        expectedGraph.add((taxon, RDF.type, OWL.Class)) 
-
-        #parse KGCL statement into data model
-        kgclModel = parser.parse(command)
-
-        graph_transformer.transform_graph(kgclModel, g)
-
-        compare_graphs(g,expectedGraph) 
-
-class TestNodeDeepening(unittest.TestCase): 
-    def test_command(self): 
-        #KGCL statement
-        command = "deepen <http://example.org/targetClass> from <http://example.org/superclass> to <http://example.org/subclass>"
-
-        #create test graph (containing only one triple) 
-        g = rdflib.Graph() 
-        targetClass = URIRef("http://example.org/targetClass") 
-        subClass = URIRef("http://example.org/subclass") 
-        superClass = URIRef("http://example.org/superclass") 
-
-        g.add((targetClass, RDFS.subClassOf, superClass)) 
-        g.add((subClass, RDFS.subClassOf, superClass)) 
-
-        expectedGraph = rdflib.Graph() 
-        expectedGraph.add((targetClass, RDFS.subClassOf, subClass)) 
-        expectedGraph.add((subClass, RDFS.subClassOf, superClass)) 
-
-        #parse KGCL statement into data model
-        kgclModel = parser.parse(command)
-
-        graph_transformer.transform_graph(kgclModel, g)
-
-        compare_graphs(g,expectedGraph) 
-
-class TestNodeShallowing(unittest.TestCase): 
-    def test_command(self): 
-        #KGCL statement
-        command = "shallow <http://example.org/targetClass> from <http://example.org/subclass> to <http://example.org/superclass>"
-
-        #create test graph (containing only one triple) 
-        g = rdflib.Graph() 
-        targetClass = URIRef("http://example.org/targetClass") 
-        subClass = URIRef("http://example.org/subclass") 
-        superClass = URIRef("http://example.org/superclass") 
-
-        g.add((targetClass, RDFS.subClassOf, subClass)) 
-        g.add((subClass, RDFS.subClassOf, superClass)) 
-
-        expectedGraph = rdflib.Graph() 
-        expectedGraph.add((targetClass, RDFS.subClassOf, superClass)) 
-        expectedGraph.add((subClass, RDFS.subClassOf, superClass)) 
-
-        #parse KGCL statement into data model
-        kgclModel = parser.parse(command)
-
-        graph_transformer.transform_graph(kgclModel, g)
-
-        compare_graphs(g,expectedGraph) 
-
-class TestNodeMove(unittest.TestCase): 
-    def test_command(self): 
-        #KGCL statement
-        command = "move <http://example.org/targetClass> from <http://example.org/A> to <http://example.org/B>"
-
-        #create test graph (containing only one triple) 
-        g = rdflib.Graph() 
-        targetClass = URIRef("http://example.org/targetClass") 
-        A = URIRef("http://example.org/A") 
-
-        g.add((targetClass, RDFS.subClassOf, A)) 
-
-        B = URIRef("http://example.org/B") 
-
-        expectedGraph = rdflib.Graph() 
-        expectedGraph.add((targetClass, RDFS.subClassOf, B)) 
-
-        #parse KGCL statement into data model
-        kgclModel = parser.parse(command)
-
-        graph_transformer.transform_graph(kgclModel, g)
-
-        compare_graphs(g,expectedGraph) 
-
-class TestEdgeCreation(unittest.TestCase): 
-    def test_command(self): 
-        #KGCL statement
-        command = "create edge <http://example.org/predicate> from <http://example.org/subject> to <http://example.org/object>"
-
-        #create test graph (containing only one triple) 
-        g = rdflib.Graph() 
-        subject = URIRef("http://example.org/subject") 
-        predicate = URIRef("http://example.org/predicate") 
-        object = URIRef("http://example.org/object") 
-
-        expectedGraph = rdflib.Graph() 
-        expectedGraph.add((subject, predicate, object)) 
-
-        #parse KGCL statement into data model
-        kgclModel = parser.parse(command)
-
-        graph_transformer.transform_graph(kgclModel, g)
-
-        compare_graphs(g,expectedGraph) 
-
-class TestEdgeDeletion(unittest.TestCase): 
-    def test_command(self): 
-        #KGCL statement
-        command = "delete edge <http://example.org/predicate> from <http://example.org/subject> to <http://example.org/object>"
-
-        #create test graph (containing only one triple) 
-        g = rdflib.Graph() 
-        subject = URIRef("http://example.org/subject") 
-        predicate = URIRef("http://example.org/predicate") 
-        object = URIRef("http://example.org/object") 
-
-        g.add((subject, predicate, object)) 
-
-        expectedGraph = rdflib.Graph() 
-
-        #parse KGCL statement into data model
-        kgclModel = parser.parse(command)
-
-        graph_transformer.transform_graph(kgclModel, g)
-
-        compare_graphs(g,expectedGraph) 
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/parser/kgcl.py b/tests/kgcl/model/kgcl.py
similarity index 99%
rename from parser/kgcl.py
rename to tests/kgcl/model/kgcl.py
index 80cdbf91..fec75fcd 100644
--- a/parser/kgcl.py
+++ b/tests/kgcl/model/kgcl.py
@@ -25,8 +25,8 @@
 from linkml_runtime.utils.enumerations import EnumDefinitionImpl
 from rdflib import Namespace, URIRef
 from linkml_runtime.utils.curienamespace import CurieNamespace
-from ontology_model import Annotation, Edge, NodeId, OntologyElement, OntologySubset, OwlTypeEnum, PropertyValue
-from prov import Activity, ActivityId
+from . ontology_model import Annotation, Edge, NodeId, OntologyElement, OntologySubset, OwlTypeEnum, PropertyValue
+from . prov import Activity, ActivityId
 from linkml_runtime.linkml_model.types import Integer, String, Uriorcurie
 from linkml_runtime.utils.metamodelcore import URIorCURIE
 
diff --git a/parser/ontology_model.py b/tests/kgcl/model/ontology_model.py
similarity index 100%
rename from parser/ontology_model.py
rename to tests/kgcl/model/ontology_model.py