rdf-construct 0.2.1__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.
Files changed (43) hide show
  1. rdf_construct/__init__.py +1 -1
  2. rdf_construct/cli.py +1794 -0
  3. rdf_construct/describe/__init__.py +93 -0
  4. rdf_construct/describe/analyzer.py +176 -0
  5. rdf_construct/describe/documentation.py +146 -0
  6. rdf_construct/describe/formatters/__init__.py +47 -0
  7. rdf_construct/describe/formatters/json.py +65 -0
  8. rdf_construct/describe/formatters/markdown.py +275 -0
  9. rdf_construct/describe/formatters/text.py +315 -0
  10. rdf_construct/describe/hierarchy.py +232 -0
  11. rdf_construct/describe/imports.py +213 -0
  12. rdf_construct/describe/metadata.py +187 -0
  13. rdf_construct/describe/metrics.py +145 -0
  14. rdf_construct/describe/models.py +552 -0
  15. rdf_construct/describe/namespaces.py +180 -0
  16. rdf_construct/describe/profiles.py +415 -0
  17. rdf_construct/localise/__init__.py +114 -0
  18. rdf_construct/localise/config.py +508 -0
  19. rdf_construct/localise/extractor.py +427 -0
  20. rdf_construct/localise/formatters/__init__.py +36 -0
  21. rdf_construct/localise/formatters/markdown.py +229 -0
  22. rdf_construct/localise/formatters/text.py +224 -0
  23. rdf_construct/localise/merger.py +346 -0
  24. rdf_construct/localise/reporter.py +356 -0
  25. rdf_construct/merge/__init__.py +165 -0
  26. rdf_construct/merge/config.py +354 -0
  27. rdf_construct/merge/conflicts.py +281 -0
  28. rdf_construct/merge/formatters.py +426 -0
  29. rdf_construct/merge/merger.py +425 -0
  30. rdf_construct/merge/migrator.py +339 -0
  31. rdf_construct/merge/rules.py +377 -0
  32. rdf_construct/merge/splitter.py +1102 -0
  33. rdf_construct/refactor/__init__.py +72 -0
  34. rdf_construct/refactor/config.py +362 -0
  35. rdf_construct/refactor/deprecator.py +328 -0
  36. rdf_construct/refactor/formatters/__init__.py +8 -0
  37. rdf_construct/refactor/formatters/text.py +311 -0
  38. rdf_construct/refactor/renamer.py +294 -0
  39. {rdf_construct-0.2.1.dist-info → rdf_construct-0.4.0.dist-info}/METADATA +91 -6
  40. {rdf_construct-0.2.1.dist-info → rdf_construct-0.4.0.dist-info}/RECORD +43 -7
  41. {rdf_construct-0.2.1.dist-info → rdf_construct-0.4.0.dist-info}/WHEEL +0 -0
  42. {rdf_construct-0.2.1.dist-info → rdf_construct-0.4.0.dist-info}/entry_points.txt +0 -0
  43. {rdf_construct-0.2.1.dist-info → rdf_construct-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,145 @@
1
+ """Basic metrics collection for ontology description.
2
+
3
+ Provides counts of classes, properties, individuals, and triples.
4
+ """
5
+
6
+ from rdflib import Graph, RDF, RDFS
7
+ from rdflib.namespace import OWL
8
+
9
+ from rdf_construct.describe.models import BasicMetrics
10
+
11
+
12
+ def collect_metrics(graph: Graph) -> BasicMetrics:
13
+ """Collect basic count metrics from an RDF graph.
14
+
15
+ Args:
16
+ graph: RDF graph to analyse.
17
+
18
+ Returns:
19
+ BasicMetrics with all count values populated.
20
+ """
21
+ # Total triples
22
+ total_triples = len(graph)
23
+
24
+ # Classes: both owl:Class and rdfs:Class (deduplicated)
25
+ owl_classes = set(graph.subjects(RDF.type, OWL.Class))
26
+ rdfs_classes = set(graph.subjects(RDF.type, RDFS.Class))
27
+ all_classes = owl_classes | rdfs_classes
28
+ classes = len(all_classes)
29
+
30
+ # Object properties
31
+ object_properties = len(set(graph.subjects(RDF.type, OWL.ObjectProperty)))
32
+
33
+ # Datatype properties
34
+ datatype_properties = len(set(graph.subjects(RDF.type, OWL.DatatypeProperty)))
35
+
36
+ # Annotation properties
37
+ annotation_properties = len(set(graph.subjects(RDF.type, OWL.AnnotationProperty)))
38
+
39
+ # RDF properties (rdf:Property not typed as OWL property)
40
+ all_rdf_props = set(graph.subjects(RDF.type, RDF.Property))
41
+ owl_props = (
42
+ set(graph.subjects(RDF.type, OWL.ObjectProperty))
43
+ | set(graph.subjects(RDF.type, OWL.DatatypeProperty))
44
+ | set(graph.subjects(RDF.type, OWL.AnnotationProperty))
45
+ )
46
+ rdf_properties = len(all_rdf_props - owl_props)
47
+
48
+ # Individuals: typed subjects that aren't classes, properties, or ontology
49
+ individuals = _count_individuals(graph, all_classes)
50
+
51
+ return BasicMetrics(
52
+ total_triples=total_triples,
53
+ classes=classes,
54
+ object_properties=object_properties,
55
+ datatype_properties=datatype_properties,
56
+ annotation_properties=annotation_properties,
57
+ rdf_properties=rdf_properties,
58
+ individuals=individuals,
59
+ )
60
+
61
+
62
+ def _count_individuals(graph: Graph, classes: set) -> int:
63
+ """Count named individuals in the graph.
64
+
65
+ Individuals are typed subjects that are not classes, properties,
66
+ or ontology declarations.
67
+
68
+ Args:
69
+ graph: RDF graph to analyse.
70
+ classes: Set of class URIs already identified.
71
+
72
+ Returns:
73
+ Count of named individuals.
74
+ """
75
+ # Gather all property URIs
76
+ properties = (
77
+ set(graph.subjects(RDF.type, OWL.ObjectProperty))
78
+ | set(graph.subjects(RDF.type, OWL.DatatypeProperty))
79
+ | set(graph.subjects(RDF.type, OWL.AnnotationProperty))
80
+ | set(graph.subjects(RDF.type, RDF.Property))
81
+ )
82
+
83
+ # Ontology declarations
84
+ ontologies = set(graph.subjects(RDF.type, OWL.Ontology))
85
+
86
+ # OWL constructs that shouldn't be counted as individuals
87
+ owl_constructs = {
88
+ OWL.Class,
89
+ OWL.ObjectProperty,
90
+ OWL.DatatypeProperty,
91
+ OWL.AnnotationProperty,
92
+ OWL.Restriction,
93
+ OWL.Ontology,
94
+ OWL.AllDisjointClasses,
95
+ OWL.AllDifferent,
96
+ OWL.NamedIndividual,
97
+ RDFS.Class,
98
+ RDF.Property,
99
+ }
100
+
101
+ # Collect all typed subjects
102
+ all_typed = set()
103
+ for s in graph.subjects(RDF.type, None):
104
+ # Skip blank nodes for counting
105
+ if hasattr(s, "n3") and not str(s).startswith("_:"):
106
+ all_typed.add(s)
107
+
108
+ # Exclude non-individuals
109
+ individuals = all_typed - classes - properties - ontologies - owl_constructs
110
+
111
+ return len(individuals)
112
+
113
+
114
+ def get_all_classes(graph: Graph) -> set:
115
+ """Get all classes from the graph.
116
+
117
+ Args:
118
+ graph: RDF graph to query.
119
+
120
+ Returns:
121
+ Set of class URIRefs (owl:Class and rdfs:Class).
122
+ """
123
+ owl_classes = set(graph.subjects(RDF.type, OWL.Class))
124
+ rdfs_classes = set(graph.subjects(RDF.type, RDFS.Class))
125
+ return owl_classes | rdfs_classes
126
+
127
+
128
+ def get_all_properties(graph: Graph) -> set:
129
+ """Get all properties from the graph.
130
+
131
+ Args:
132
+ graph: RDF graph to query.
133
+
134
+ Returns:
135
+ Set of property URIRefs (all types).
136
+ """
137
+ props = set()
138
+ for prop_type in (
139
+ OWL.ObjectProperty,
140
+ OWL.DatatypeProperty,
141
+ OWL.AnnotationProperty,
142
+ RDF.Property,
143
+ ):
144
+ props |= set(graph.subjects(RDF.type, prop_type))
145
+ return props
@@ -0,0 +1,552 @@
1
+ """Data models for ontology description analysis.
2
+
3
+ Provides dataclasses representing all analysis results from the describe
4
+ command, designed for easy serialization to JSON and formatting.
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+ from datetime import datetime
9
+ from enum import Enum
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+
14
+ class OntologyProfile(Enum):
15
+ """Detected ontology profile/expressiveness level.
16
+
17
+ Ordered from least to most expressive. Each level implies
18
+ capability for reasoning at that level.
19
+ """
20
+
21
+ RDF = "rdf"
22
+ RDFS = "rdfs"
23
+ OWL_DL_SIMPLE = "owl_dl_simple"
24
+ OWL_DL_EXPRESSIVE = "owl_dl_expressive"
25
+ OWL_FULL = "owl_full"
26
+
27
+ @property
28
+ def display_name(self) -> str:
29
+ """Human-readable name for the profile."""
30
+ names = {
31
+ OntologyProfile.RDF: "RDF",
32
+ OntologyProfile.RDFS: "RDFS",
33
+ OntologyProfile.OWL_DL_SIMPLE: "OWL 2 DL (simple)",
34
+ OntologyProfile.OWL_DL_EXPRESSIVE: "OWL 2 DL (expressive)",
35
+ OntologyProfile.OWL_FULL: "OWL 2 Full",
36
+ }
37
+ return names[self]
38
+
39
+ @property
40
+ def reasoning_guidance(self) -> str:
41
+ """Guidance on reasoning value for this profile."""
42
+ guidance = {
43
+ OntologyProfile.RDF: "No schema; reasoning not applicable",
44
+ OntologyProfile.RDFS: "Subclass/subproperty inference available",
45
+ OntologyProfile.OWL_DL_SIMPLE: "Standard DL reasoning; efficient",
46
+ OntologyProfile.OWL_DL_EXPRESSIVE: "Full DL reasoning; may be computationally expensive",
47
+ OntologyProfile.OWL_FULL: "Undecidable; reasoning may not terminate",
48
+ }
49
+ return guidance[self]
50
+
51
+
52
+ class NamespaceCategory(Enum):
53
+ """Category of namespace usage in the ontology."""
54
+
55
+ LOCAL = "local" # Defined in this ontology
56
+ IMPORTED = "imported" # Declared via owl:imports
57
+ EXTERNAL = "external" # Referenced but not imported
58
+
59
+
60
+ class ImportStatus(Enum):
61
+ """Status of an import resolution check."""
62
+
63
+ RESOLVABLE = "resolvable"
64
+ UNRESOLVABLE = "unresolvable"
65
+ UNCHECKED = "unchecked"
66
+
67
+
68
+ @dataclass
69
+ class OntologyMetadata:
70
+ """Extracted ontology metadata.
71
+
72
+ Attributes:
73
+ ontology_iri: The ontology IRI (from owl:Ontology declaration).
74
+ version_iri: The version IRI (owl:versionIRI), if present.
75
+ title: Human-readable title from rdfs:label, dcterms:title, or dc:title.
76
+ description: Description from rdfs:comment, dcterms:description, or dc:description.
77
+ license_uri: License URI, if declared.
78
+ license_label: License label, if available.
79
+ creators: List of creator names/IRIs.
80
+ version_info: Version string from owl:versionInfo.
81
+ """
82
+
83
+ ontology_iri: str | None = None
84
+ version_iri: str | None = None
85
+ title: str | None = None
86
+ description: str | None = None
87
+ license_uri: str | None = None
88
+ license_label: str | None = None
89
+ creators: list[str] = field(default_factory=list)
90
+ version_info: str | None = None
91
+
92
+ @property
93
+ def has_iri(self) -> bool:
94
+ """Whether an ontology IRI is declared."""
95
+ return self.ontology_iri is not None
96
+
97
+ def to_dict(self) -> dict[str, Any]:
98
+ """Convert to dictionary for JSON serialisation."""
99
+ return {
100
+ "ontology_iri": self.ontology_iri,
101
+ "version_iri": self.version_iri,
102
+ "title": self.title,
103
+ "description": self.description,
104
+ "license_uri": self.license_uri,
105
+ "license_label": self.license_label,
106
+ "creators": self.creators,
107
+ "version_info": self.version_info,
108
+ }
109
+
110
+
111
+ @dataclass
112
+ class BasicMetrics:
113
+ """Basic ontology metrics.
114
+
115
+ Attributes:
116
+ total_triples: Total triple count in the graph.
117
+ classes: Number of classes (owl:Class + rdfs:Class).
118
+ object_properties: Number of owl:ObjectProperty entities.
119
+ datatype_properties: Number of owl:DatatypeProperty entities.
120
+ annotation_properties: Number of owl:AnnotationProperty entities.
121
+ rdf_properties: Number of rdf:Property entities (not typed as OWL).
122
+ individuals: Number of named individuals.
123
+ """
124
+
125
+ total_triples: int = 0
126
+ classes: int = 0
127
+ object_properties: int = 0
128
+ datatype_properties: int = 0
129
+ annotation_properties: int = 0
130
+ rdf_properties: int = 0
131
+ individuals: int = 0
132
+
133
+ @property
134
+ def total_properties(self) -> int:
135
+ """Total count of all property types."""
136
+ return (
137
+ self.object_properties
138
+ + self.datatype_properties
139
+ + self.annotation_properties
140
+ + self.rdf_properties
141
+ )
142
+
143
+ @property
144
+ def summary_line(self) -> str:
145
+ """One-line summary of metrics."""
146
+ parts = [f"{self.total_triples} triples"]
147
+ if self.classes:
148
+ parts.append(f"{self.classes} classes")
149
+ if self.total_properties:
150
+ parts.append(f"{self.total_properties} properties")
151
+ if self.individuals:
152
+ parts.append(f"{self.individuals} individuals")
153
+ return ", ".join(parts)
154
+
155
+ def to_dict(self) -> dict[str, Any]:
156
+ """Convert to dictionary for JSON serialisation."""
157
+ return {
158
+ "total_triples": self.total_triples,
159
+ "classes": self.classes,
160
+ "object_properties": self.object_properties,
161
+ "datatype_properties": self.datatype_properties,
162
+ "annotation_properties": self.annotation_properties,
163
+ "rdf_properties": self.rdf_properties,
164
+ "individuals": self.individuals,
165
+ "total_properties": self.total_properties,
166
+ }
167
+
168
+
169
+ @dataclass
170
+ class ProfileDetection:
171
+ """Result of ontology profile detection.
172
+
173
+ Attributes:
174
+ profile: Detected ontology profile.
175
+ detected_features: List of features that influenced detection.
176
+ owl_constructs_found: Specific OWL constructs found (for DL/Full distinction).
177
+ violating_constructs: Constructs that pushed profile to OWL Full.
178
+ """
179
+
180
+ profile: OntologyProfile = OntologyProfile.RDF
181
+ detected_features: list[str] = field(default_factory=list)
182
+ owl_constructs_found: list[str] = field(default_factory=list)
183
+ violating_constructs: list[str] = field(default_factory=list)
184
+
185
+ @property
186
+ def display_name(self) -> str:
187
+ """Human-readable profile name."""
188
+ return self.profile.display_name
189
+
190
+ @property
191
+ def reasoning_guidance(self) -> str:
192
+ """Guidance on reasoning value."""
193
+ return self.profile.reasoning_guidance
194
+
195
+ def to_dict(self) -> dict[str, Any]:
196
+ """Convert to dictionary for JSON serialisation."""
197
+ return {
198
+ "profile": self.profile.value,
199
+ "display_name": self.display_name,
200
+ "reasoning_guidance": self.reasoning_guidance,
201
+ "detected_features": self.detected_features,
202
+ "owl_constructs_found": self.owl_constructs_found,
203
+ "violating_constructs": self.violating_constructs,
204
+ }
205
+
206
+
207
+ @dataclass
208
+ class NamespaceInfo:
209
+ """Information about a namespace used in the ontology.
210
+
211
+ Attributes:
212
+ uri: The namespace URI.
213
+ prefix: Bound prefix (if any).
214
+ category: Whether local, imported, or external.
215
+ usage_count: Number of times URIs from this namespace appear.
216
+ """
217
+
218
+ uri: str
219
+ prefix: str | None = None
220
+ category: NamespaceCategory = NamespaceCategory.EXTERNAL
221
+ usage_count: int = 0
222
+
223
+ def to_dict(self) -> dict[str, Any]:
224
+ """Convert to dictionary for JSON serialisation."""
225
+ return {
226
+ "uri": self.uri,
227
+ "prefix": self.prefix,
228
+ "category": self.category.value,
229
+ "usage_count": self.usage_count,
230
+ }
231
+
232
+
233
+ @dataclass
234
+ class NamespaceAnalysis:
235
+ """Complete namespace analysis for the ontology.
236
+
237
+ Attributes:
238
+ local_namespace: The primary/local namespace (if identifiable).
239
+ namespaces: List of all namespaces found.
240
+ unimported_external: Namespaces that are used but not imported.
241
+ """
242
+
243
+ local_namespace: str | None = None
244
+ namespaces: list[NamespaceInfo] = field(default_factory=list)
245
+ unimported_external: list[str] = field(default_factory=list)
246
+
247
+ @property
248
+ def local_count(self) -> int:
249
+ """Number of local namespaces."""
250
+ return sum(1 for ns in self.namespaces if ns.category == NamespaceCategory.LOCAL)
251
+
252
+ @property
253
+ def imported_count(self) -> int:
254
+ """Number of imported namespaces."""
255
+ return sum(1 for ns in self.namespaces if ns.category == NamespaceCategory.IMPORTED)
256
+
257
+ @property
258
+ def external_count(self) -> int:
259
+ """Number of external (unimported) namespaces."""
260
+ return sum(1 for ns in self.namespaces if ns.category == NamespaceCategory.EXTERNAL)
261
+
262
+ def to_dict(self) -> dict[str, Any]:
263
+ """Convert to dictionary for JSON serialisation."""
264
+ return {
265
+ "local_namespace": self.local_namespace,
266
+ "namespaces": [ns.to_dict() for ns in self.namespaces],
267
+ "unimported_external": self.unimported_external,
268
+ "counts": {
269
+ "local": self.local_count,
270
+ "imported": self.imported_count,
271
+ "external": self.external_count,
272
+ },
273
+ }
274
+
275
+
276
+ @dataclass
277
+ class ImportInfo:
278
+ """Information about a declared import.
279
+
280
+ Attributes:
281
+ uri: The imported ontology URI.
282
+ status: Resolution status.
283
+ error: Error message if unresolvable.
284
+ """
285
+
286
+ uri: str
287
+ status: ImportStatus = ImportStatus.UNCHECKED
288
+ error: str | None = None
289
+
290
+ @property
291
+ def is_resolvable(self) -> bool:
292
+ """Whether the import was resolved successfully."""
293
+ return self.status == ImportStatus.RESOLVABLE
294
+
295
+ def to_dict(self) -> dict[str, Any]:
296
+ """Convert to dictionary for JSON serialisation."""
297
+ return {
298
+ "uri": self.uri,
299
+ "status": self.status.value,
300
+ "error": self.error,
301
+ }
302
+
303
+
304
+ @dataclass
305
+ class ImportAnalysis:
306
+ """Analysis of ontology imports.
307
+
308
+ Attributes:
309
+ imports: List of declared imports with resolution status.
310
+ resolve_attempted: Whether resolution was attempted.
311
+ """
312
+
313
+ imports: list[ImportInfo] = field(default_factory=list)
314
+ resolve_attempted: bool = False
315
+
316
+ @property
317
+ def count(self) -> int:
318
+ """Number of declared imports."""
319
+ return len(self.imports)
320
+
321
+ @property
322
+ def resolvable_count(self) -> int:
323
+ """Number of resolvable imports."""
324
+ return sum(1 for imp in self.imports if imp.is_resolvable)
325
+
326
+ @property
327
+ def unresolvable_count(self) -> int:
328
+ """Number of unresolvable imports."""
329
+ return sum(
330
+ 1 for imp in self.imports if imp.status == ImportStatus.UNRESOLVABLE
331
+ )
332
+
333
+ def to_dict(self) -> dict[str, Any]:
334
+ """Convert to dictionary for JSON serialisation."""
335
+ return {
336
+ "imports": [imp.to_dict() for imp in self.imports],
337
+ "resolve_attempted": self.resolve_attempted,
338
+ "counts": {
339
+ "total": self.count,
340
+ "resolvable": self.resolvable_count if self.resolve_attempted else None,
341
+ "unresolvable": self.unresolvable_count if self.resolve_attempted else None,
342
+ },
343
+ }
344
+
345
+
346
+ @dataclass
347
+ class HierarchyAnalysis:
348
+ """Class hierarchy analysis.
349
+
350
+ Attributes:
351
+ root_classes: Classes with no superclass (except owl:Thing/rdfs:Resource).
352
+ max_depth: Maximum depth of the hierarchy.
353
+ orphan_classes: Classes that are neither parent nor child of anything.
354
+ has_cycles: Whether cycles were detected.
355
+ cycle_members: URIs involved in cycles (if any).
356
+ """
357
+
358
+ root_classes: list[str] = field(default_factory=list)
359
+ max_depth: int = 0
360
+ orphan_classes: list[str] = field(default_factory=list)
361
+ has_cycles: bool = False
362
+ cycle_members: list[str] = field(default_factory=list)
363
+
364
+ @property
365
+ def root_count(self) -> int:
366
+ """Number of root classes."""
367
+ return len(self.root_classes)
368
+
369
+ @property
370
+ def orphan_count(self) -> int:
371
+ """Number of orphan classes."""
372
+ return len(self.orphan_classes)
373
+
374
+ def to_dict(self) -> dict[str, Any]:
375
+ """Convert to dictionary for JSON serialisation."""
376
+ return {
377
+ "root_classes": self.root_classes,
378
+ "root_count": self.root_count,
379
+ "max_depth": self.max_depth,
380
+ "orphan_classes": self.orphan_classes,
381
+ "orphan_count": self.orphan_count,
382
+ "has_cycles": self.has_cycles,
383
+ "cycle_members": self.cycle_members,
384
+ }
385
+
386
+
387
+ @dataclass
388
+ class DocumentationCoverage:
389
+ """Documentation coverage metrics.
390
+
391
+ Attributes:
392
+ classes_with_label: Number of classes with rdfs:label.
393
+ classes_total: Total number of classes.
394
+ classes_with_definition: Number of classes with rdfs:comment or skos:definition.
395
+ properties_with_label: Number of properties with rdfs:label.
396
+ properties_total: Total number of properties.
397
+ properties_with_definition: Number of properties with definitions.
398
+ """
399
+
400
+ classes_with_label: int = 0
401
+ classes_total: int = 0
402
+ classes_with_definition: int = 0
403
+ properties_with_label: int = 0
404
+ properties_total: int = 0
405
+ properties_with_definition: int = 0
406
+
407
+ @property
408
+ def class_label_pct(self) -> float:
409
+ """Percentage of classes with labels."""
410
+ if self.classes_total == 0:
411
+ return 100.0
412
+ return (self.classes_with_label / self.classes_total) * 100
413
+
414
+ @property
415
+ def class_definition_pct(self) -> float:
416
+ """Percentage of classes with definitions."""
417
+ if self.classes_total == 0:
418
+ return 100.0
419
+ return (self.classes_with_definition / self.classes_total) * 100
420
+
421
+ @property
422
+ def property_label_pct(self) -> float:
423
+ """Percentage of properties with labels."""
424
+ if self.properties_total == 0:
425
+ return 100.0
426
+ return (self.properties_with_label / self.properties_total) * 100
427
+
428
+ @property
429
+ def property_definition_pct(self) -> float:
430
+ """Percentage of properties with definitions."""
431
+ if self.properties_total == 0:
432
+ return 100.0
433
+ return (self.properties_with_definition / self.properties_total) * 100
434
+
435
+ def to_dict(self) -> dict[str, Any]:
436
+ """Convert to dictionary for JSON serialisation."""
437
+ return {
438
+ "classes": {
439
+ "with_label": self.classes_with_label,
440
+ "with_definition": self.classes_with_definition,
441
+ "total": self.classes_total,
442
+ "label_pct": round(self.class_label_pct, 1),
443
+ "definition_pct": round(self.class_definition_pct, 1),
444
+ },
445
+ "properties": {
446
+ "with_label": self.properties_with_label,
447
+ "with_definition": self.properties_with_definition,
448
+ "total": self.properties_total,
449
+ "label_pct": round(self.property_label_pct, 1),
450
+ "definition_pct": round(self.property_definition_pct, 1),
451
+ },
452
+ }
453
+
454
+
455
+ @dataclass
456
+ class ReasoningAnalysis:
457
+ """Analysis of reasoning implications (optional, off by default).
458
+
459
+ Attributes:
460
+ entailment_regime: Applicable entailment regime.
461
+ inferred_superclasses: Sample of superclass inferences.
462
+ inferred_types: Sample of type inferences.
463
+ consistency_notes: Notes on potential consistency issues.
464
+ """
465
+
466
+ entailment_regime: str = "none"
467
+ inferred_superclasses: list[str] = field(default_factory=list)
468
+ inferred_types: list[str] = field(default_factory=list)
469
+ consistency_notes: list[str] = field(default_factory=list)
470
+
471
+ def to_dict(self) -> dict[str, Any]:
472
+ """Convert to dictionary for JSON serialisation."""
473
+ return {
474
+ "entailment_regime": self.entailment_regime,
475
+ "inferred_superclasses": self.inferred_superclasses,
476
+ "inferred_types": self.inferred_types,
477
+ "consistency_notes": self.consistency_notes,
478
+ }
479
+
480
+
481
+ @dataclass
482
+ class OntologyDescription:
483
+ """Complete description of an ontology.
484
+
485
+ Aggregates all analysis results into a single structure for
486
+ formatting and serialisation.
487
+ """
488
+
489
+ # Source information
490
+ source: str | Path
491
+ timestamp: datetime = field(default_factory=datetime.now)
492
+
493
+ # Analysis results
494
+ metadata: OntologyMetadata = field(default_factory=OntologyMetadata)
495
+ metrics: BasicMetrics = field(default_factory=BasicMetrics)
496
+ profile: ProfileDetection = field(default_factory=ProfileDetection)
497
+ namespaces: NamespaceAnalysis = field(default_factory=NamespaceAnalysis)
498
+ imports: ImportAnalysis = field(default_factory=ImportAnalysis)
499
+ hierarchy: HierarchyAnalysis = field(default_factory=HierarchyAnalysis)
500
+ documentation: DocumentationCoverage = field(default_factory=DocumentationCoverage)
501
+ reasoning: ReasoningAnalysis | None = None
502
+
503
+ # Analysis settings
504
+ brief: bool = False
505
+ include_reasoning: bool = False
506
+
507
+ @property
508
+ def verdict(self) -> str:
509
+ """One-line summary verdict for the ontology."""
510
+ parts = []
511
+
512
+ # Profile
513
+ parts.append(self.profile.display_name)
514
+
515
+ # Size
516
+ parts.append(self.metrics.summary_line)
517
+
518
+ # Import status
519
+ if self.imports.count > 0:
520
+ if self.imports.resolve_attempted:
521
+ if self.imports.unresolvable_count > 0:
522
+ parts.append(
523
+ f"{self.imports.unresolvable_count}/{self.imports.count} imports unresolvable"
524
+ )
525
+ else:
526
+ parts.append(f"{self.imports.count} imports OK")
527
+ else:
528
+ parts.append(f"{self.imports.count} imports")
529
+
530
+ return " | ".join(parts)
531
+
532
+ def to_dict(self) -> dict[str, Any]:
533
+ """Convert to dictionary for JSON serialisation."""
534
+ result = {
535
+ "source": str(self.source),
536
+ "timestamp": self.timestamp.isoformat(),
537
+ "verdict": self.verdict,
538
+ "metadata": self.metadata.to_dict(),
539
+ "metrics": self.metrics.to_dict(),
540
+ "profile": self.profile.to_dict(),
541
+ }
542
+
543
+ if not self.brief:
544
+ result["namespaces"] = self.namespaces.to_dict()
545
+ result["imports"] = self.imports.to_dict()
546
+ result["hierarchy"] = self.hierarchy.to_dict()
547
+ result["documentation"] = self.documentation.to_dict()
548
+
549
+ if self.reasoning is not None:
550
+ result["reasoning"] = self.reasoning.to_dict()
551
+
552
+ return result