rdf-construct 0.3.0__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,180 @@
1
+ """Namespace analysis for ontology description.
2
+
3
+ Categorises namespaces as local, imported, or external (unimported).
4
+ """
5
+
6
+ from collections import Counter
7
+
8
+ from rdflib import Graph, URIRef, BNode
9
+ from rdflib.namespace import OWL, RDF, RDFS, XSD
10
+
11
+ from rdf_construct.describe.models import (
12
+ NamespaceAnalysis,
13
+ NamespaceInfo,
14
+ NamespaceCategory,
15
+ )
16
+ from rdf_construct.describe.imports import get_imported_namespaces
17
+
18
+
19
+ # Well-known vocabulary namespaces (always considered external but expected)
20
+ WELL_KNOWN_NAMESPACES = {
21
+ str(RDF): "rdf",
22
+ str(RDFS): "rdfs",
23
+ str(OWL): "owl",
24
+ str(XSD): "xsd",
25
+ "http://www.w3.org/2004/02/skos/core#": "skos",
26
+ "http://purl.org/dc/elements/1.1/": "dc",
27
+ "http://purl.org/dc/terms/": "dcterms",
28
+ "http://xmlns.com/foaf/0.1/": "foaf",
29
+ "http://www.w3.org/ns/prov#": "prov",
30
+ "http://www.w3.org/ns/shacl#": "sh",
31
+ }
32
+
33
+
34
+ def analyse_namespaces(graph: Graph) -> NamespaceAnalysis:
35
+ """Analyse namespace usage in the ontology.
36
+
37
+ Categorises each namespace as:
38
+ - LOCAL: Defined in this ontology (contains defined classes/properties)
39
+ - IMPORTED: Declared via owl:imports
40
+ - EXTERNAL: Referenced but not imported (may indicate missing import)
41
+
42
+ Args:
43
+ graph: RDF graph to analyse.
44
+
45
+ Returns:
46
+ NamespaceAnalysis with categorised namespaces.
47
+ """
48
+ # Get ontology IRI to identify local namespace
49
+ local_namespace = _get_local_namespace(graph)
50
+
51
+ # Get imported namespaces
52
+ imported_ns = get_imported_namespaces(graph)
53
+
54
+ # Count namespace usage across all triples
55
+ ns_usage = _count_namespace_usage(graph)
56
+
57
+ # Get prefix bindings
58
+ prefix_map = {str(uri): prefix for prefix, uri in graph.namespace_manager.namespaces()}
59
+
60
+ # Build namespace info list
61
+ namespaces: list[NamespaceInfo] = []
62
+ unimported_external: list[str] = []
63
+
64
+ for ns_uri, count in sorted(ns_usage.items(), key=lambda x: -x[1]):
65
+ # Determine category
66
+ if local_namespace and ns_uri == local_namespace:
67
+ category = NamespaceCategory.LOCAL
68
+ elif ns_uri in imported_ns:
69
+ category = NamespaceCategory.IMPORTED
70
+ elif ns_uri in WELL_KNOWN_NAMESPACES:
71
+ # Well-known vocabularies are external but expected
72
+ category = NamespaceCategory.EXTERNAL
73
+ else:
74
+ # Unknown external namespace - might be missing import
75
+ category = NamespaceCategory.EXTERNAL
76
+ if ns_uri not in WELL_KNOWN_NAMESPACES:
77
+ unimported_external.append(ns_uri)
78
+
79
+ # Get prefix (from bindings or well-known)
80
+ prefix = prefix_map.get(ns_uri) or WELL_KNOWN_NAMESPACES.get(ns_uri)
81
+
82
+ namespaces.append(NamespaceInfo(
83
+ uri=ns_uri,
84
+ prefix=prefix,
85
+ category=category,
86
+ usage_count=count,
87
+ ))
88
+
89
+ return NamespaceAnalysis(
90
+ local_namespace=local_namespace,
91
+ namespaces=namespaces,
92
+ unimported_external=unimported_external,
93
+ )
94
+
95
+
96
+ def _get_local_namespace(graph: Graph) -> str | None:
97
+ """Determine the local/primary namespace for the ontology.
98
+
99
+ Uses the ontology IRI if declared, otherwise infers from
100
+ most commonly used namespace for defined entities.
101
+
102
+ Args:
103
+ graph: RDF graph to analyse.
104
+
105
+ Returns:
106
+ Local namespace URI or None if not determinable.
107
+ """
108
+ # First, try to get from owl:Ontology declaration
109
+ for ontology in graph.subjects(RDF.type, OWL.Ontology):
110
+ if isinstance(ontology, URIRef):
111
+ return _extract_namespace(str(ontology))
112
+
113
+ # Fall back to most common namespace for defined classes
114
+ class_ns = Counter[str]()
115
+ for cls in graph.subjects(RDF.type, OWL.Class):
116
+ if isinstance(cls, URIRef):
117
+ ns = _extract_namespace(str(cls))
118
+ if ns not in WELL_KNOWN_NAMESPACES:
119
+ class_ns[ns] += 1
120
+
121
+ for cls in graph.subjects(RDF.type, RDFS.Class):
122
+ if isinstance(cls, URIRef):
123
+ ns = _extract_namespace(str(cls))
124
+ if ns not in WELL_KNOWN_NAMESPACES:
125
+ class_ns[ns] += 1
126
+
127
+ if class_ns:
128
+ return class_ns.most_common(1)[0][0]
129
+
130
+ return None
131
+
132
+
133
+ def _count_namespace_usage(graph: Graph) -> dict[str, int]:
134
+ """Count how many times each namespace is used in the graph.
135
+
136
+ Args:
137
+ graph: RDF graph to analyse.
138
+
139
+ Returns:
140
+ Dictionary mapping namespace URI to usage count.
141
+ """
142
+ ns_count: Counter[str] = Counter()
143
+
144
+ for s, p, o in graph:
145
+ # Count subject namespace (skip blank nodes)
146
+ if isinstance(s, URIRef):
147
+ ns_count[_extract_namespace(str(s))] += 1
148
+
149
+ # Count predicate namespace
150
+ if isinstance(p, URIRef):
151
+ ns_count[_extract_namespace(str(p))] += 1
152
+
153
+ # Count object namespace (if URI)
154
+ if isinstance(o, URIRef):
155
+ ns_count[_extract_namespace(str(o))] += 1
156
+
157
+ return dict(ns_count)
158
+
159
+
160
+ def _extract_namespace(uri: str) -> str:
161
+ """Extract namespace from a URI.
162
+
163
+ Handles both # and / as namespace separators.
164
+
165
+ Args:
166
+ uri: Full URI string.
167
+
168
+ Returns:
169
+ Namespace portion of the URI.
170
+ """
171
+ # Split on # first (OWL-style namespaces)
172
+ if "#" in uri:
173
+ return uri.rsplit("#", 1)[0] + "#"
174
+
175
+ # Otherwise split on last /
176
+ if "/" in uri:
177
+ return uri.rsplit("/", 1)[0] + "/"
178
+
179
+ # No separator found, return as-is
180
+ return uri
@@ -0,0 +1,415 @@
1
+ """OWL profile detection for ontologies.
2
+
3
+ Detects whether an ontology is RDF, RDFS, OWL 2 DL (simple or expressive),
4
+ or OWL 2 Full based on the constructs used.
5
+ """
6
+
7
+ from rdflib import Graph, RDF, RDFS, URIRef
8
+ from rdflib.namespace import OWL, XSD
9
+
10
+ from rdf_construct.describe.models import OntologyProfile, ProfileDetection
11
+
12
+
13
+ # OWL constructs that indicate OWL usage (but not necessarily expressive)
14
+ OWL_BASIC_CONSTRUCTS = {
15
+ OWL.Class,
16
+ OWL.ObjectProperty,
17
+ OWL.DatatypeProperty,
18
+ OWL.AnnotationProperty,
19
+ OWL.Ontology,
20
+ OWL.NamedIndividual,
21
+ OWL.Thing,
22
+ OWL.Nothing,
23
+ }
24
+
25
+ # Property characteristics (simple OWL DL)
26
+ OWL_PROPERTY_CHARACTERISTICS = {
27
+ OWL.FunctionalProperty,
28
+ OWL.InverseFunctionalProperty,
29
+ OWL.TransitiveProperty,
30
+ OWL.SymmetricProperty,
31
+ OWL.AsymmetricProperty,
32
+ OWL.ReflexiveProperty,
33
+ OWL.IrreflexiveProperty,
34
+ }
35
+
36
+ # Expressive OWL constructs (restrictions, equivalences, etc.)
37
+ OWL_EXPRESSIVE_CONSTRUCTS = {
38
+ OWL.Restriction,
39
+ OWL.equivalentClass,
40
+ OWL.disjointWith,
41
+ OWL.AllDisjointClasses,
42
+ OWL.AllDifferent,
43
+ OWL.unionOf,
44
+ OWL.intersectionOf,
45
+ OWL.complementOf,
46
+ OWL.oneOf,
47
+ OWL.hasValue,
48
+ OWL.someValuesFrom,
49
+ OWL.allValuesFrom,
50
+ OWL.minCardinality,
51
+ OWL.maxCardinality,
52
+ OWL.cardinality,
53
+ OWL.minQualifiedCardinality,
54
+ OWL.maxQualifiedCardinality,
55
+ OWL.qualifiedCardinality,
56
+ OWL.hasSelf,
57
+ OWL.propertyChainAxiom,
58
+ OWL.hasKey,
59
+ }
60
+
61
+ # OWL 2 Full constructs (constructs that violate DL constraints)
62
+ # These indicate usage patterns that are undecidable
63
+ OWL_FULL_INDICATORS = {
64
+ # Classes used as individuals or vice versa (punning beyond DL limits)
65
+ # Using owl:Class as a property
66
+ # Self-referential class definitions
67
+ # Using rdf:type with complex expressions
68
+ }
69
+
70
+ # RDFS constructs that indicate RDFS-level usage
71
+ RDFS_CONSTRUCTS = {
72
+ RDFS.Class,
73
+ RDFS.subClassOf,
74
+ RDFS.subPropertyOf,
75
+ RDFS.domain,
76
+ RDFS.range,
77
+ RDFS.label,
78
+ RDFS.comment,
79
+ }
80
+
81
+ # Well-known vocabulary namespaces that don't indicate OWL usage
82
+ VOCABULARY_NAMESPACES = {
83
+ str(RDF),
84
+ str(RDFS),
85
+ str(OWL),
86
+ str(XSD),
87
+ "http://www.w3.org/2004/02/skos/core#",
88
+ "http://purl.org/dc/elements/1.1/",
89
+ "http://purl.org/dc/terms/",
90
+ "http://xmlns.com/foaf/0.1/",
91
+ "http://www.w3.org/ns/prov#",
92
+ }
93
+
94
+
95
+ def detect_profile(graph: Graph) -> ProfileDetection:
96
+ """Detect the OWL profile of an ontology.
97
+
98
+ Analyses the constructs used in the ontology to determine its
99
+ expressiveness level.
100
+
101
+ Args:
102
+ graph: RDF graph to analyse.
103
+
104
+ Returns:
105
+ ProfileDetection with profile and supporting evidence.
106
+ """
107
+ detected_features: list[str] = []
108
+ owl_constructs: list[str] = []
109
+ violating_constructs: list[str] = []
110
+
111
+ # Check for OWL Full indicators first (these are most restrictive)
112
+ owl_full_issues = _detect_owl_full(graph)
113
+ if owl_full_issues:
114
+ violating_constructs.extend(owl_full_issues)
115
+ return ProfileDetection(
116
+ profile=OntologyProfile.OWL_FULL,
117
+ detected_features=["OWL Full constructs detected"],
118
+ owl_constructs_found=owl_constructs,
119
+ violating_constructs=violating_constructs,
120
+ )
121
+
122
+ # Check for expressive OWL constructs
123
+ expressive_found = _find_expressive_constructs(graph)
124
+ if expressive_found:
125
+ owl_constructs.extend(expressive_found)
126
+ detected_features.append("Expressive OWL constructs (restrictions, equivalences)")
127
+ return ProfileDetection(
128
+ profile=OntologyProfile.OWL_DL_EXPRESSIVE,
129
+ detected_features=detected_features,
130
+ owl_constructs_found=owl_constructs,
131
+ violating_constructs=[],
132
+ )
133
+
134
+ # Check for basic OWL constructs
135
+ basic_owl_found = _find_basic_owl_constructs(graph)
136
+ property_chars = _find_property_characteristics(graph)
137
+
138
+ if basic_owl_found or property_chars:
139
+ owl_constructs.extend(basic_owl_found)
140
+ owl_constructs.extend(property_chars)
141
+
142
+ if property_chars:
143
+ detected_features.append("Property characteristics declared")
144
+ if basic_owl_found:
145
+ detected_features.append("OWL class/property declarations")
146
+
147
+ return ProfileDetection(
148
+ profile=OntologyProfile.OWL_DL_SIMPLE,
149
+ detected_features=detected_features,
150
+ owl_constructs_found=owl_constructs,
151
+ violating_constructs=[],
152
+ )
153
+
154
+ # Check for RDFS constructs
155
+ rdfs_found = _find_rdfs_constructs(graph)
156
+ if rdfs_found:
157
+ detected_features.append("RDFS vocabulary in use")
158
+ return ProfileDetection(
159
+ profile=OntologyProfile.RDFS,
160
+ detected_features=detected_features,
161
+ owl_constructs_found=[],
162
+ violating_constructs=[],
163
+ )
164
+
165
+ # Default to pure RDF
166
+ detected_features.append("No schema constructs found")
167
+ return ProfileDetection(
168
+ profile=OntologyProfile.RDF,
169
+ detected_features=detected_features,
170
+ owl_constructs_found=[],
171
+ violating_constructs=[],
172
+ )
173
+
174
+
175
+ def _detect_owl_full(graph: Graph) -> list[str]:
176
+ """Detect constructs that indicate OWL Full.
177
+
178
+ OWL Full allows patterns that are undecidable, including:
179
+ - Metaclasses (classes that are instances of other classes)
180
+ - Properties with classes as values
181
+ - Circular definitions in certain ways
182
+
183
+ Args:
184
+ graph: RDF graph to analyse.
185
+
186
+ Returns:
187
+ List of OWL Full indicator descriptions.
188
+ """
189
+ issues: list[str] = []
190
+
191
+ # Check for metaclasses: classes that are rdf:type of other classes
192
+ # This is a common OWL Full pattern
193
+ owl_classes = set(graph.subjects(RDF.type, OWL.Class))
194
+ rdfs_classes = set(graph.subjects(RDF.type, RDFS.Class))
195
+ all_classes = owl_classes | rdfs_classes
196
+
197
+ for cls in all_classes:
198
+ # Check if this class is an instance of another class (not owl:Class/rdfs:Class)
199
+ for class_type in graph.objects(cls, RDF.type):
200
+ if class_type not in {OWL.Class, RDFS.Class} and class_type in all_classes:
201
+ issues.append(f"Metaclass: {_curie(graph, cls)} is instance of class {_curie(graph, class_type)}")
202
+
203
+ # Check for owl:Class used in unexpected positions
204
+ # For example, as the object of a property that expects individuals
205
+ for s, p, o in graph:
206
+ # Skip type assertions
207
+ if p == RDF.type:
208
+ continue
209
+
210
+ # If object is a class and predicate domain/range suggests individuals
211
+ if o in all_classes:
212
+ # Check if predicate is an object property with individual range
213
+ if (p, RDF.type, OWL.ObjectProperty) in graph:
214
+ prop_range = list(graph.objects(p, RDFS.range))
215
+ # If range is defined and is not a class of classes, this might be Full
216
+ # This is a simplified check; full analysis would require more inference
217
+
218
+ # Check for problematic self-reference patterns
219
+ # e.g., C owl:equivalentClass [ owl:complementOf C ] could be problematic
220
+ for cls in owl_classes:
221
+ equiv_classes = list(graph.objects(cls, OWL.equivalentClass))
222
+ for equiv in equiv_classes:
223
+ # Check for direct self-equivalence to complement
224
+ complement = list(graph.objects(equiv, OWL.complementOf))
225
+ if cls in complement:
226
+ issues.append(f"Self-contradictory equivalence: {_curie(graph, cls)}")
227
+
228
+ return issues
229
+
230
+
231
+ def _find_expressive_constructs(graph: Graph) -> list[str]:
232
+ """Find expressive OWL constructs in the graph.
233
+
234
+ Args:
235
+ graph: RDF graph to analyse.
236
+
237
+ Returns:
238
+ List of expressive construct descriptions found.
239
+ """
240
+ found: list[str] = []
241
+
242
+ # Check for restrictions
243
+ restrictions = set(graph.subjects(RDF.type, OWL.Restriction))
244
+ if restrictions:
245
+ found.append(f"owl:Restriction ({len(restrictions)} found)")
246
+
247
+ # Check for class equivalences
248
+ equiv_count = len(list(graph.subject_objects(OWL.equivalentClass)))
249
+ if equiv_count:
250
+ found.append(f"owl:equivalentClass ({equiv_count} axioms)")
251
+
252
+ # Check for disjointness
253
+ disjoint_count = len(list(graph.subject_objects(OWL.disjointWith)))
254
+ if disjoint_count:
255
+ found.append(f"owl:disjointWith ({disjoint_count} axioms)")
256
+
257
+ all_disjoint = set(graph.subjects(RDF.type, OWL.AllDisjointClasses))
258
+ if all_disjoint:
259
+ found.append(f"owl:AllDisjointClasses ({len(all_disjoint)} found)")
260
+
261
+ # Check for set operators
262
+ union_of = len(list(graph.subject_objects(OWL.unionOf)))
263
+ if union_of:
264
+ found.append(f"owl:unionOf ({union_of} uses)")
265
+
266
+ intersection_of = len(list(graph.subject_objects(OWL.intersectionOf)))
267
+ if intersection_of:
268
+ found.append(f"owl:intersectionOf ({intersection_of} uses)")
269
+
270
+ complement_of = len(list(graph.subject_objects(OWL.complementOf)))
271
+ if complement_of:
272
+ found.append(f"owl:complementOf ({complement_of} uses)")
273
+
274
+ one_of = len(list(graph.subject_objects(OWL.oneOf)))
275
+ if one_of:
276
+ found.append(f"owl:oneOf ({one_of} uses)")
277
+
278
+ # Check for property chains
279
+ chains = len(list(graph.subject_objects(OWL.propertyChainAxiom)))
280
+ if chains:
281
+ found.append(f"owl:propertyChainAxiom ({chains} chains)")
282
+
283
+ # Check for keys
284
+ keys = len(list(graph.subject_objects(OWL.hasKey)))
285
+ if keys:
286
+ found.append(f"owl:hasKey ({keys} keys)")
287
+
288
+ return found
289
+
290
+
291
+ def _find_basic_owl_constructs(graph: Graph) -> list[str]:
292
+ """Find basic OWL constructs in the graph.
293
+
294
+ Args:
295
+ graph: RDF graph to analyse.
296
+
297
+ Returns:
298
+ List of basic OWL construct descriptions found.
299
+ """
300
+ found: list[str] = []
301
+
302
+ # Check for owl:Ontology
303
+ ontologies = set(graph.subjects(RDF.type, OWL.Ontology))
304
+ if ontologies:
305
+ found.append("owl:Ontology declaration")
306
+
307
+ # Check for owl:Class (distinct from rdfs:Class)
308
+ owl_classes = set(graph.subjects(RDF.type, OWL.Class))
309
+ if owl_classes:
310
+ found.append(f"owl:Class ({len(owl_classes)} found)")
311
+
312
+ # Check for OWL property types
313
+ obj_props = set(graph.subjects(RDF.type, OWL.ObjectProperty))
314
+ if obj_props:
315
+ found.append(f"owl:ObjectProperty ({len(obj_props)} found)")
316
+
317
+ data_props = set(graph.subjects(RDF.type, OWL.DatatypeProperty))
318
+ if data_props:
319
+ found.append(f"owl:DatatypeProperty ({len(data_props)} found)")
320
+
321
+ ann_props = set(graph.subjects(RDF.type, OWL.AnnotationProperty))
322
+ if ann_props:
323
+ found.append(f"owl:AnnotationProperty ({len(ann_props)} found)")
324
+
325
+ # Check for named individuals
326
+ individuals = set(graph.subjects(RDF.type, OWL.NamedIndividual))
327
+ if individuals:
328
+ found.append(f"owl:NamedIndividual ({len(individuals)} found)")
329
+
330
+ return found
331
+
332
+
333
+ def _find_property_characteristics(graph: Graph) -> list[str]:
334
+ """Find OWL property characteristic declarations.
335
+
336
+ Args:
337
+ graph: RDF graph to analyse.
338
+
339
+ Returns:
340
+ List of property characteristic descriptions found.
341
+ """
342
+ found: list[str] = []
343
+
344
+ for char in OWL_PROPERTY_CHARACTERISTICS:
345
+ props = set(graph.subjects(RDF.type, char))
346
+ if props:
347
+ # Get local name of the characteristic
348
+ char_name = str(char).split("#")[-1]
349
+ found.append(f"owl:{char_name} ({len(props)} properties)")
350
+
351
+ # Check for inverse properties
352
+ inverse_of = len(list(graph.subject_objects(OWL.inverseOf)))
353
+ if inverse_of:
354
+ found.append(f"owl:inverseOf ({inverse_of} pairs)")
355
+
356
+ return found
357
+
358
+
359
+ def _find_rdfs_constructs(graph: Graph) -> list[str]:
360
+ """Find RDFS-level constructs in the graph.
361
+
362
+ Args:
363
+ graph: RDF graph to analyse.
364
+
365
+ Returns:
366
+ List of RDFS construct descriptions found.
367
+ """
368
+ found: list[str] = []
369
+
370
+ # Check for rdfs:Class (not owl:Class)
371
+ rdfs_classes = set(graph.subjects(RDF.type, RDFS.Class))
372
+ owl_classes = set(graph.subjects(RDF.type, OWL.Class))
373
+ pure_rdfs = rdfs_classes - owl_classes
374
+ if pure_rdfs:
375
+ found.append(f"rdfs:Class ({len(pure_rdfs)} found)")
376
+
377
+ # Check for subclass assertions
378
+ subclass = len(list(graph.subject_objects(RDFS.subClassOf)))
379
+ if subclass:
380
+ found.append(f"rdfs:subClassOf ({subclass} axioms)")
381
+
382
+ # Check for subproperty assertions
383
+ subprop = len(list(graph.subject_objects(RDFS.subPropertyOf)))
384
+ if subprop:
385
+ found.append(f"rdfs:subPropertyOf ({subprop} axioms)")
386
+
387
+ # Check for domain/range
388
+ domain = len(list(graph.subject_objects(RDFS.domain)))
389
+ range_count = len(list(graph.subject_objects(RDFS.range)))
390
+ if domain or range_count:
391
+ found.append(f"Domain/range declarations ({domain + range_count} total)")
392
+
393
+ return found
394
+
395
+
396
+ def _curie(graph: Graph, uri: URIRef) -> str:
397
+ """Convert URI to CURIE or short form for display.
398
+
399
+ Args:
400
+ graph: Graph with namespace bindings.
401
+ uri: URI to convert.
402
+
403
+ Returns:
404
+ CURIE or shortened URI string.
405
+ """
406
+ try:
407
+ return graph.namespace_manager.normalizeUri(uri)
408
+ except Exception:
409
+ # Fall back to just the local name
410
+ s = str(uri)
411
+ if "#" in s:
412
+ return s.split("#")[-1]
413
+ elif "/" in s:
414
+ return s.split("/")[-1]
415
+ return s
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rdf-construct
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Semantic RDF manipulation toolkit - order, serialize, and diff RDF ontologies
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -50,6 +50,7 @@ Description-Content-Type: text/markdown
50
50
  ## Features
51
51
 
52
52
  - **Semantic Ordering**: Serialise RDF/Turtle with intelligent ordering instead of alphabetical chaos
53
+ - **Ontology Description**: Quick orientation to unfamiliar ontologies with profile detection
53
54
  - **Documentation Generation**: Create navigable HTML, Markdown, or JSON documentation from ontologies
54
55
  - **UML Generation**: Create PlantUML class diagrams from RDF ontologies
55
56
  - **PUML2RDF**: Convert PlantUML diagrams to RDF/OWL ontologies (diagram-first design)
@@ -64,6 +65,7 @@ Description-Content-Type: text/markdown
64
65
  - **Ontology Statistics**: Comprehensive metrics with comparison mode
65
66
  - **Flexible Styling**: Configure colours, layouts, and visual themes for diagrams
66
67
  - **Profile-Based**: Define multiple strategies in YAML configuration
68
+ - **Multi-Format Input**: Supports Turtle, RDF/XML, JSON-LD, N-Triples
67
69
  - **Deterministic**: Same input + profile = same output, always
68
70
 
69
71
  ## Why?
@@ -93,6 +95,19 @@ pip install -e .
93
95
  poetry install
94
96
  ```
95
97
 
98
+ ### Describe an Ontology
99
+
100
+ ```bash
101
+ # Quick orientation to an unfamiliar ontology
102
+ rdf-construct describe ontology.ttl
103
+
104
+ # Brief summary (metadata + metrics + profile)
105
+ rdf-construct describe ontology.ttl --brief
106
+
107
+ # JSON output for scripting
108
+ rdf-construct describe ontology.ttl --format json
109
+ ```
110
+
96
111
  ### Compare Ontology Versions
97
112
 
98
113
  ```bash
@@ -243,15 +258,16 @@ rdf-construct localise report ontology.ttl --languages en,de,fr
243
258
 
244
259
  **For Users**:
245
260
  - [Getting Started](docs/user_guides/GETTING_STARTED.md) - 5-minute quick start
261
+ - [Describe Guide](docs/user_guides/DESCRIBE_GUIDE.md) - Quick ontology orientation
246
262
  - [Docs Guide](docs/user_guides/DOCS_GUIDE.md) - Documentation generation
247
263
  - [UML Guide](docs/user_guides/UML_GUIDE.md) - Complete UML features
248
- - [PlantUML Import Guide](docs/user_guides/PLANTUML_IMPORT_GUIDE.md) - Diagram-first design
264
+ - [PUML2RDF Guide](docs/user_guides/PUML2RDF_GUIDE.md) - Diagram-first design
249
265
  - [SHACL Guide](docs/user_guides/SHACL_GUIDE.md) - SHACL shape generation
250
266
  - [Diff Guide](docs/user_guides/DIFF_GUIDE.md) - Semantic ontology comparison
251
267
  - [Lint Guide](docs/user_guides/LINT_GUIDE.md) - Ontology quality checking
252
268
  - [CQ Testing Guide](docs/user_guides/CQ_TEST_GUIDE.md) - Competency question testing
253
269
  - [Stats Guide](docs/user_guides/STATS_GUIDE.md) - Ontology metrics
254
- - [Merge Guide](docs/user_guides/MERGE_GUIDE.md) - Combining ontologies
270
+ - [Merge & Split Guide](docs/user_guides/MERGE_SPLIT_GUIDE.md) - Combining and modularising ontologies
255
271
  - [Refactor Guide](docs/user_guides/REFACTOR_GUIDE.md) - Renaming and deprecation
256
272
  - [Localise Guide](docs/user_guides/LOCALISE_GUIDE.md) - Multi-language translations
257
273
  - [CLI Reference](docs/user_guides/CLI_REFERENCE.md) - All commands and options
@@ -409,7 +425,7 @@ properties:
409
425
 
410
426
  ## Project Status
411
427
 
412
- **Current**: v0.2.0 - Feature complete for core ontology workflows
428
+ **Current**: v0.4.0 - Feature complete for core ontology workflows
413
429
  **License**: MIT
414
430
 
415
431
  ### Implemented
@@ -425,10 +441,15 @@ properties:
425
441
  ✅ Ontology linting (11 rules)
426
442
  ✅ Competency question testing
427
443
  ✅ Ontology statistics
444
+ ✅ Ontology merging and splitting
445
+ ✅ Ontology refactoring (rename, deprecate)
446
+ ✅ Multi-language translation management
447
+ ✅ Ontology description and profile detection
448
+ ✅ Multi-format input support (Turtle, RDF/XML, JSON-LD, N-Triples)
428
449
  ✅ Comprehensive documentation
429
450
 
430
451
  ### (Possible) Roadmap
431
- - [ ] Multi-format input support (JSON-LD, RDF/XML)
452
+ - [ ] OWL 2 named profile detection (EL, RL, QL)
432
453
  - [ ] Streaming mode for very large graphs
433
454
  - [ ] Web UI for diagram configuration
434
455
  - [ ] Additional lint rules
@@ -491,6 +512,7 @@ MIT License - see [LICENSE](LICENSE) file for details.
491
512
 
492
513
  ---
493
514
 
494
- **Status**: v0.2.0
515
+ **Status**: v0.4.0
495
516
  **Python**: 3.10+ required
496
517
  **Maintainer**: See [CONTRIBUTING.md](CONTRIBUTING.md)
518
+