diff --git a/ontology/uco/observable/observable.ttl b/ontology/uco/observable/observable.ttl
index 9d141ecd..7ed93d93 100644
--- a/ontology/uco/observable/observable.ttl
+++ b/ontology/uco/observable/observable.ttl
@@ -6,6 +6,7 @@
# imports: https://ontology.unifiedcyberontology.org/uco/vocabulary
@prefix action: .
+@prefix co: .
@prefix core: .
@prefix identity: .
@prefix location: .
@@ -3863,9 +3864,20 @@ observable:MessageThread
owl:Class ,
sh:NodeShape
;
- rdfs:subClassOf observable:ObservableObject ;
+ rdfs:subClassOf
+ observable:ObservableObject ,
+ types:Thread
+ ;
rdfs:label "MessageTread"@en ;
rdfs:comment "A message thread is a running commentary of electronic messages pertaining to one topic or question."@en ;
+ sh:property [
+ sh:class observable:Message ;
+ sh:description "A MessageThread's items' contents must be Message objects."@en ;
+ sh:path (
+ co:item
+ co:itemContent
+ ) ;
+ ] ;
sh:targetClass observable:MessageThread ;
.
diff --git a/ontology/uco/types/types.ttl b/ontology/uco/types/types.ttl
index ce5877b7..aa9574f6 100644
--- a/ontology/uco/types/types.ttl
+++ b/ontology/uco/types/types.ttl
@@ -1,6 +1,9 @@
+# imports: https://ontology.unifiedcyberontology.org/co
# imports: https://ontology.unifiedcyberontology.org/uco/core
# imports: https://ontology.unifiedcyberontology.org/uco/vocabulary
+@prefix co: .
+@prefix core: .
@prefix owl: .
@prefix rdf: .
@prefix rdfs: .
@@ -13,6 +16,7 @@
a owl:Ontology ;
rdfs:label "uco-types"@en ;
owl:imports
+ ,
,
;
@@ -169,6 +173,42 @@ types:StructuredText
rdfs:comment "Expresses string-based data in some information structuring format (e.g., HTML5)."@en ;
.
+types:Thread
+ a
+ owl:Class ,
+ sh:NodeShape
+ ;
+ rdfs:subClassOf
+ co:Bag ,
+ core:UcoObject
+ ;
+ rdfs:label "Thread"@en ;
+ rdfs:comment "A semi-ordered array of items, that can be present in multiple copies. Implemetation of a UCO Thread is similar to a Collections Ontology List, except a Thread may fork and merge - that is, one of its members may have two or more direct successors, and two or more direct predecessors."@en ;
+ owl:disjointWith co:List ;
+ sh:property [
+ sh:class types:ThreadItem ;
+ sh:path co:item ;
+ ] ;
+ .
+
+types:ThreadItem
+ a
+ owl:Class ,
+ sh:NodeShape
+ ;
+ rdfs:subClassOf
+ co:Item ,
+ core:UcoObject
+ ;
+ rdfs:label "ThreadItem"@en ;
+ rdfs:comment "A ThreadItem is a member of a thread."@en ;
+ owl:disjointWith co:ListItem ;
+ sh:property [
+ sh:class core:UcoObject ;
+ sh:path co:itemContent ;
+ ] ;
+ .
+
types:entry
a owl:ObjectProperty ;
rdfs:label "entry"@en ;
@@ -209,6 +249,178 @@ types:key
rdfs:range xsd:string ;
.
+types:threadNextItem
+ a owl:ObjectProperty ;
+ rdfs:subPropertyOf types:threadSuccessor ;
+ rdfs:label "threadNextItem"@en ;
+ rdfs:comment "The link to a next item in a thread."@en ;
+ rdfs:seeAlso co:nextItem ;
+ .
+
+types:threadNextItem-subjects-shape
+ a sh:PropertyShape ;
+ sh:class types:ThreadItem ;
+ sh:nodeKind sh:BlankNodeOrIRI ;
+ sh:path types:threadNextItem ;
+ sh:targetSubjectsOf types:threadNextItem ;
+ .
+
+types:threadOriginItem
+ a owl:ObjectProperty ;
+ rdfs:subPropertyOf co:item ;
+ rdfs:label "threadOriginItem"@en ;
+ rdfs:comment "A link to an item of the thread known to have no predecessor."@en ;
+ rdfs:domain types:Thread ;
+ rdfs:range [
+ a owl:Class ;
+ owl:intersectionOf (
+ types:ThreadItem
+ [
+ a owl:Restriction ;
+ owl:onProperty types:threadPreviousItem ;
+ owl:cardinality "0"^^xsd:nonNegativeInteger ;
+ ]
+ ) ;
+ ] ;
+ rdfs:seeAlso co:firstItem ;
+ .
+
+types:threadOriginItem-subjects-shape
+ a sh:PropertyShape ;
+ sh:class types:ThreadItem ;
+ sh:path types:threadOriginItem ;
+ sh:targetSubjectsOf types:threadOriginItem ;
+ .
+
+types:threadOriginItem-subjects-threadPredecessor-shape
+ a sh:PropertyShape ;
+ sh:description "An origin item in a thread must not have a predecessor."@en ;
+ sh:maxCount "0"^^xsd:integer ;
+ sh:path (
+ types:threadOriginItem
+ types:threadPredecessor
+ ) ;
+ sh:targetSubjectsOf types:threadOriginItem ;
+ .
+
+types:threadOriginItem-subjects-threadPreviousItem-shape
+ a sh:PropertyShape ;
+ sh:description "An origin item in a thread must not have a previous item."@en ;
+ sh:maxCount "0"^^xsd:integer ;
+ sh:path (
+ types:threadOriginItem
+ types:threadPreviousItem
+ ) ;
+ sh:targetSubjectsOf types:threadOriginItem ;
+ .
+
+types:threadPredecessor
+ a
+ owl:ObjectProperty ,
+ owl:TransitiveProperty
+ ;
+ rdfs:label "threadPredecessor"@en ;
+ rdfs:comment "The link to the preceding item in a thread."@en ;
+ rdfs:domain types:ThreadItem ;
+ rdfs:range types:ThreadItem ;
+ rdfs:seeAlso co:precededBy ;
+ owl:inverseOf types:threadSuccessor ;
+ .
+
+types:threadPredecessor-subjects-shape
+ a sh:PropertyShape ;
+ sh:class types:ThreadItem ;
+ sh:nodeKind sh:BlankNodeOrIRI ;
+ sh:path types:threadPredecessor ;
+ sh:targetSubjectsOf types:threadPredecessor ;
+ .
+
+types:threadPreviousItem
+ a owl:ObjectProperty ;
+ rdfs:subPropertyOf types:threadPredecessor ;
+ rdfs:label "threadPreviousItem"@en ;
+ rdfs:comment "A direct link to a previous item in a thread."@en ;
+ rdfs:seeAlso co:previousItem ;
+ owl:inverseOf types:threadNextItem ;
+ .
+
+types:threadPreviousItem-subjects-shape
+ a sh:PropertyShape ;
+ sh:class types:ThreadItem ;
+ sh:nodeKind sh:BlankNodeOrIRI ;
+ sh:path types:threadPreviousItem ;
+ sh:targetSubjectsOf types:threadPreviousItem ;
+ .
+
+types:threadSuccessor
+ a
+ owl:ObjectProperty ,
+ owl:TransitiveProperty
+ ;
+ rdfs:label "threadSuccessor"@en ;
+ rdfs:comment "A link to a following item in a thread."@en ;
+ rdfs:domain types:ThreadItem ;
+ rdfs:range types:ThreadItem ;
+ rdfs:seeAlso co:followedBy ;
+ .
+
+types:threadSuccessor-subjects-shape
+ a sh:PropertyShape ;
+ sh:class types:ThreadItem ;
+ sh:nodeKind sh:BlankNodeOrIRI ;
+ sh:path types:threadSuccessor ;
+ sh:targetSubjectsOf types:threadSuccessor ;
+ .
+
+types:threadTerminalItem
+ a owl:ObjectProperty ;
+ rdfs:subPropertyOf co:item ;
+ rdfs:label "threadTerminalItem"@en ;
+ rdfs:comment "A link to an item of the thread known to have no successor."@en ;
+ rdfs:domain types:Thread ;
+ rdfs:range [
+ a owl:Class ;
+ owl:intersectionOf (
+ types:ThreadItem
+ [
+ a owl:Restriction ;
+ owl:onProperty types:threadNextItem ;
+ owl:cardinality "0"^^xsd:nonNegativeInteger ;
+ ]
+ ) ;
+ ] ;
+ rdfs:seeAlso co:lastItem ;
+ .
+
+types:threadTerminalItem-subjects-shape
+ a sh:PropertyShape ;
+ sh:class types:ThreadItem ;
+ sh:path types:threadTerminalItem ;
+ sh:targetSubjectsOf types:threadTerminalItem ;
+ .
+
+types:threadTerminalItem-subjects-threadNextItem-shape
+ a sh:PropertyShape ;
+ sh:description "A terminal item in a thread must not have a next item."@en ;
+ sh:maxCount "0"^^xsd:integer ;
+ sh:path (
+ types:threadTerminalItem
+ types:threadNextItem
+ ) ;
+ sh:targetSubjectsOf types:threadTerminalItem ;
+ .
+
+types:threadTerminalItem-subjects-threadSuccessor-shape
+ a sh:PropertyShape ;
+ sh:description "A terminal item in a thread must not have a successor."@en ;
+ sh:maxCount "0"^^xsd:integer ;
+ sh:path (
+ types:threadTerminalItem
+ types:threadSuccessor
+ ) ;
+ sh:targetSubjectsOf types:threadTerminalItem ;
+ .
+
types:value
a owl:DatatypeProperty ;
rdfs:label "value"@en ;
diff --git a/tests/examples/Makefile b/tests/examples/Makefile
index 260e1e18..8db1bb8f 100644
--- a/tests/examples/Makefile
+++ b/tests/examples/Makefile
@@ -27,8 +27,11 @@ all: \
hash_XFAIL_validation.ttl \
location_PASS_validation.ttl \
location_XFAIL_validation.ttl \
+ message_thread_PASS_validation.ttl \
relationship_PASS_validation.ttl \
- relationship_XFAIL_validation.ttl
+ relationship_XFAIL_validation.ttl \
+ thread_PASS_validation.ttl \
+ thread_XFAIL_validation.ttl
.PRECIOUS: \
%_validation.ttl
@@ -78,8 +81,11 @@ check: \
hash_XFAIL_validation.ttl \
location_PASS_validation.ttl \
location_XFAIL_validation.ttl \
+ message_thread_PASS_validation.ttl \
relationship_PASS_validation.ttl \
- relationship_XFAIL_validation.ttl
+ relationship_XFAIL_validation.ttl \
+ thread_PASS_validation.ttl \
+ thread_XFAIL_validation.ttl
source $(tests_srcdir)/venv/bin/activate \
&& pytest \
--log-level=DEBUG
diff --git a/tests/examples/message_thread_PASS.json b/tests/examples/message_thread_PASS.json
new file mode 100644
index 00000000..947acb4e
--- /dev/null
+++ b/tests/examples/message_thread_PASS.json
@@ -0,0 +1,165 @@
+{
+ "@context": {
+ "co": "http://purl.org/co/",
+ "kb": "http://example.org/kb/",
+ "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
+ "observable": "https://ontology.unifiedcyberontology.org/uco/observable/",
+ "types": "https://ontology.unifiedcyberontology.org/uco/types/",
+ "xsd": "http://www.w3.org/2001/XMLSchema#"
+ },
+ "@graph": [
+ {
+ "@id": "kb:message-1",
+ "@type": "observable:Message"
+ },
+ {
+ "@id": "kb:message-2",
+ "@type": "observable:Message"
+ },
+ {
+ "@id": "kb:message-3",
+ "@type": "observable:Message"
+ },
+ {
+ "@id": "kb:message-4",
+ "@type": "observable:Message"
+ },
+ {
+ "@id": "kb:message-5",
+ "@type": "observable:Message"
+ },
+ {
+ "@id": "kb:message-6",
+ "@type": "observable:Message"
+ },
+ {
+ "@id": "kb:message-7",
+ "@type": "observable:Message"
+ },
+ {
+ "@id": "kb:message-thread-1",
+ "@type": "observable:MessageThread",
+ "co:size": {
+ "@type": "xsd:nonNegativeInteger",
+ "@value": "6"
+ },
+ "co:item": [
+ {
+ "@id": "kb:message-thread-1-item-1"
+ },
+ {
+ "@id": "kb:message-thread-1-item-2"
+ },
+ {
+ "@id": "kb:message-thread-1-item-3"
+ },
+ {
+ "@id": "kb:message-thread-1-item-4"
+ },
+ {
+ "@id": "kb:message-thread-1-item-5"
+ },
+ {
+ "@id": "kb:message-thread-1-item-6"
+ }
+ ],
+ "types:threadOriginItem": [
+ {
+ "@id": "kb:message-thread-1-item-1"
+ },
+ {
+ "@id": "kb:message-thread-1-item-5"
+ }
+ ],
+ "types:threadTerminalItem": [
+ {
+ "@id": "kb:message-thread-1-item-3"
+ },
+ {
+ "@id": "kb:message-thread-1-item-4"
+ },
+ {
+ "@id": "kb:message-thread-1-item-6"
+ }
+ ]
+ },
+ {
+ "@id": "kb:message-thread-1-item-1",
+ "@type": "types:ThreadItem",
+ "co:itemContent": {
+ "@id": "kb:message-1"
+ },
+ "types:threadNextItem": [
+ {
+ "@id": "kb:message-thread-1-item-2"
+ },
+ {
+ "@id": "kb:message-thread-1-item-6"
+ }
+ ]
+ },
+ {
+ "@id": "kb:message-thread-1-item-2",
+ "@type": "types:ThreadItem",
+ "co:itemContent": {
+ "@id": "kb:message-2"
+ },
+ "types:threadNextItem": [
+ {
+ "@id": "kb:message-thread-1-item-3"
+ },
+ {
+ "@id": "kb:message-thread-1-item-4"
+ }
+ ],
+ "types:threadPreviousItem": {
+ "@id": "kb:message-thread-1-item-1"
+ }
+ },
+ {
+ "@id": "kb:message-thread-1-item-3",
+ "@type": "types:ThreadItem",
+ "co:itemContent": {
+ "@id": "kb:message-3"
+ },
+ "types:threadPreviousItem": {
+ "@id": "kb:message-thread-1-item-2"
+ }
+ },
+ {
+ "@id": "kb:message-thread-1-item-4",
+ "@type": "types:ThreadItem",
+ "co:itemContent": {
+ "@id": "kb:message-4"
+ },
+ "types:threadPreviousItem": {
+ "@id": "kb:message-thread-1-item-2"
+ }
+ },
+ {
+ "@id": "kb:message-thread-1-item-5",
+ "@type": "types:ThreadItem",
+ "co:itemContent": {
+ "@id": "kb:message-5"
+ },
+ "types:threadNextItem": {
+ "@id": "kb:message-thread-1-item-6"
+ }
+ },
+ {
+ "@id": "kb:message-thread-1-item-6",
+ "@type": "types:ThreadItem",
+ "co:itemContent": {
+ "@id": "kb:message-6"
+ },
+ "types:threadPreviousItem": [
+ {
+ "@id": "kb:message-thread-1-item-1"
+ },
+ {
+ "@id": "kb:message-thread-1-item-5"
+ }
+ ]
+ }
+ ]
+}
diff --git a/tests/examples/test_validation.py b/tests/examples/test_validation.py
index 44b30335..b48cc765 100644
--- a/tests/examples/test_validation.py
+++ b/tests/examples/test_validation.py
@@ -21,6 +21,7 @@
the only functions to be called to be named "test_*".
"""
+import pathlib
import logging
import typing
@@ -33,9 +34,17 @@
NS_UCO_CO = rdflib.Namespace("https://ontology.unifiedcyberontology.org/co/")
NS_UCO_CORE = rdflib.Namespace("https://ontology.unifiedcyberontology.org/uco/core/")
NS_UCO_LOCATION = rdflib.Namespace("https://ontology.unifiedcyberontology.org/uco/location/")
+NS_UCO_TYPES = rdflib.Namespace("https://ontology.unifiedcyberontology.org/uco/types/")
NSDICT = {"sh": NS_SH}
+@pytest.fixture(scope="session")
+def monolithic_ontology_graph() -> rdflib.Graph:
+ graph = rdflib.Graph()
+ monolithic_ttl_path = pathlib.Path(__file__).parent.parent / "uco_monolithic.ttl"
+ graph.parse(str(monolithic_ttl_path), format="turtle")
+ return graph
+
def load_validation_graph(
filename : str,
expected_conformance : bool
@@ -250,6 +259,64 @@ def test_location_XFAIL_validation_XPASS_wrong_concept_name():
}
)
+def test_message_thread(monolithic_ontology_graph: rdflib.Graph) -> None:
+ r"""
+ Confirm the answer to this question:
+ What are all of the messages that followed the first in the thread kb:message-thread-1?
+
+ message-thread-1 forked, and has these reply paths:
+
+ 1 2 3
+ * --- * --- *
+ \ \
+ \ \ 4
+ \ *
+ 5 \ 6
+ * --- *
+
+ 7
+ *
+
+ (Message 7 is outside the thread.)
+ """
+
+ expected: typing.Set[str] = {
+ "http://example.org/kb/message-2",
+ "http://example.org/kb/message-3",
+ "http://example.org/kb/message-4",
+ "http://example.org/kb/message-6",
+ }
+ computed: typing.Set[str] = set()
+
+ data_graph = rdflib.Graph()
+ data_filepath = pathlib.Path(__file__).parent / "message_thread_PASS.json"
+ data_graph.parse(str(data_filepath), format="json-ld")
+
+ analysis_graph = data_graph + monolithic_ontology_graph
+
+ query_str = """\
+PREFIX co:
+PREFIX kb:
+PREFIX rdfs:
+PREFIX types:
+
+SELECT ?nLaterMessage
+WHERE {
+ ?nFirstMessageItem
+ co:itemContent kb:message-1 ;
+ (types:threadNextItem|types:threadSuccessor)+ / co:itemContent ?nLaterMessage ;
+ .
+}
+"""
+
+ for result in analysis_graph.query(query_str):
+ computed.add(str(result[0]))
+
+ assert expected == computed
+
+def test_message_thread_PASS_validation():
+ confirm_validation_results("message_thread_PASS_validation.ttl", True)
+
def test_relationship_PASS_partial() -> None:
"""
This test should be replaced with test_relationship_XFAIL_full when the semi-open vocabulary design current as of UCO 0.8.0 is re-done.
@@ -318,3 +385,17 @@ def test_relationship_XFAIL_full() -> None:
("http://example.org/kb/relationship-2-3-2", str(NS_SH.Violation)),
}
)
+
+def test_thread_PASS_validation():
+ confirm_validation_results("thread_PASS_validation.ttl", True)
+
+def test_thread_XFAIL_validation():
+ confirm_validation_results(
+ "thread_XFAIL_validation.ttl",
+ False,
+ expected_result_paths={
+ str(NS_CO.item),
+ str(NS_CO.itemContent),
+ str(NS_UCO_TYPES.threadOriginItem),
+ }
+ )
diff --git a/tests/examples/thread_PASS.json b/tests/examples/thread_PASS.json
new file mode 100644
index 00000000..a13a296c
--- /dev/null
+++ b/tests/examples/thread_PASS.json
@@ -0,0 +1,52 @@
+{
+ "@context": {
+ "co": "http://purl.org/co/",
+ "kb": "http://example.org/kb/",
+ "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
+ "types": "https://ontology.unifiedcyberontology.org/uco/types/",
+ "xsd": "http://www.w3.org/2001/XMLSchema#"
+ },
+ "@graph": [
+ {
+ "@id": "kb:thread-1",
+ "@type": "types:Thread",
+ "co:item": [
+ {
+ "@id": "kb:thread-1-item-1"
+ },
+ {
+ "@id": "kb:thread-1-item-2"
+ },
+ {
+ "@id": "kb:thread-1-item-3"
+ }
+ ]
+ },
+ {
+ "@id": "kb:thread-1-item-1",
+ "@type": "types:ThreadItem",
+ "types:threadNextItem": [
+ {
+ "@id": "kb:thread-1-item-2"
+ },
+ {
+ "@id": "kb:thread-1-item-3"
+ }
+ ]
+ },
+ {
+ "@id": "kb:thread-1-item-2",
+ "@type": "types:ThreadItem",
+ "types:threadPreviousItem": {
+ "@id": "kb:thread-1-item-1"
+ }
+ },
+ {
+ "@id": "kb:thread-1-item-3",
+ "@type": "types:ThreadItem",
+ "types:threadPreviousItem": {
+ "@id": "kb:thread-1-item-1"
+ }
+ }
+ ]
+}
diff --git a/tests/examples/thread_XFAIL.json b/tests/examples/thread_XFAIL.json
new file mode 100644
index 00000000..7a844ad3
--- /dev/null
+++ b/tests/examples/thread_XFAIL.json
@@ -0,0 +1,113 @@
+{
+ "@context": {
+ "co": "http://purl.org/co/",
+ "core": "https://ontology.unifiedcyberontology.org/uco/core/",
+ "kb": "http://example.org/kb/",
+ "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
+ "types": "https://ontology.unifiedcyberontology.org/uco/types/",
+ "xsd": "http://www.w3.org/2001/XMLSchema#"
+ },
+ "@graph": [
+ {
+ "@id": "kb:uco-object-4",
+ "@type": "core:UcoObject"
+ },
+ {
+ "@id": "kb:uco-object-5",
+ "@type": "core:UcoObject"
+ },
+ {
+ "@id": "kb:thread-2",
+ "@type": "types:Thread",
+ "types:threadOriginItem": {
+ "@id": "kb:uco-object-4"
+ },
+ "co:item": {
+ "@id": "kb:uco-object-4"
+ },
+ "rdfs:comment": [
+ "Error - types:threadOriginItem is not a types:ThreadItem.",
+ "Error - co:item is not a co:Item."
+ ]
+ },
+ {
+ "@id": "kb:thread-3",
+ "@type": "types:Thread",
+ "co:item": [
+ {
+ "@id": "kb:thread-3-item-1"
+ },
+ {
+ "@id": "kb:thread-3-item-2"
+ }
+ ]
+ },
+ {
+ "@id": "kb:thread-3-item-1",
+ "@type": "types:ThreadItem",
+ "co:itemContent": {
+ "@id": "kb:thread-3-item-2"
+ },
+ "rdfs:comment": [
+ "Error - co:itemContent must not be a co:Item (and types:ThreadItem is a subclass of co:Item)."
+ ]
+ },
+ {
+ "@id": "kb:thread-3-item-2",
+ "@type": "types:ThreadItem",
+ "co:itemContent": [
+ {
+ "@id": "kb:uco-object-4"
+ },
+ {
+ "@id": "kb:uco-object-5"
+ }
+ ],
+ "rdfs:comment": [
+ "Error - 2 values of co:itemContent."
+ ]
+ },
+ {
+ "@id": "kb:thread-4",
+ "@type": "types:Thread",
+ "co:item": [
+ {
+ "@id": "kb:thread-4-item-1"
+ },
+ {
+ "@id": "kb:thread-4-item-2"
+ },
+ {
+ "@id": "kb:thread-4-item-3"
+ }
+ ],
+ "rdfs:comment": "Error - list forks in opposite direction of what types:Thread supports, with item 3 directly preceeded by 1 and 2."
+ },
+ {
+ "@id": "kb:thread-4-item-1",
+ "@type": "types:ThreadItem",
+ "types:threadNextItem": {
+ "@id": "kb:thread-4-item-3"
+ }
+ },
+ {
+ "@id": "kb:thread-4-item-2",
+ "@type": "types:ThreadItem",
+ "types:threadNextItem": {
+ "@id": "kb:thread-4-item-3"
+ }
+ },
+ {
+ "@id": "kb:thread-4-item-3",
+ "@type": "types:ThreadItem",
+ "types:threadPreviousItem": [
+ {
+ "@id": "kb:thread-4-item-1"
+ },
+ {
+ "@id": "kb:thread-4-item-2"
+ }
+ ]
+ }
+ ]
+}