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,328 @@
1
+ """Deprecation workflow for ontology entities.
2
+
3
+ This module handles marking ontology entities as deprecated:
4
+ - Adds owl:deprecated true
5
+ - Adds dcterms:isReplacedBy with replacement URI
6
+ - Updates rdfs:comment with deprecation notice
7
+ - Preserves all existing properties
8
+
9
+ Deprecation marks entities but does NOT rename or migrate references.
10
+ Use 'refactor rename' to actually migrate references after deprecation.
11
+ """
12
+
13
+ from dataclasses import dataclass, field
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ from rdflib import Graph, URIRef, Literal, Namespace
18
+ from rdflib.namespace import RDF, RDFS, OWL, DCTERMS
19
+
20
+ from rdf_construct.refactor.config import DeprecationSpec, DeprecationConfig
21
+
22
+
23
+ # Dublin Core Terms namespace
24
+ DCTERMS = Namespace("http://purl.org/dc/terms/")
25
+
26
+
27
+ @dataclass
28
+ class EntityDeprecationInfo:
29
+ """Information about a deprecated entity.
30
+
31
+ Attributes:
32
+ uri: URI of the deprecated entity.
33
+ found: Whether the entity was found in the graph.
34
+ current_labels: Current rdfs:label values.
35
+ current_comments: Current rdfs:comment values.
36
+ was_already_deprecated: Whether owl:deprecated was already present.
37
+ triples_added: Number of triples added.
38
+ reference_count: Number of references to this entity in the graph.
39
+ replaced_by: Replacement entity URI (if specified).
40
+ message: Deprecation message added.
41
+ """
42
+
43
+ uri: str
44
+ found: bool = True
45
+ current_labels: list[str] = field(default_factory=list)
46
+ current_comments: list[str] = field(default_factory=list)
47
+ was_already_deprecated: bool = False
48
+ triples_added: int = 0
49
+ reference_count: int = 0
50
+ replaced_by: str | None = None
51
+ message: str | None = None
52
+
53
+
54
+ @dataclass
55
+ class DeprecationStats:
56
+ """Statistics from a deprecation operation.
57
+
58
+ Attributes:
59
+ entities_deprecated: Number of entities marked deprecated.
60
+ entities_not_found: Number of entities not found in graph.
61
+ entities_already_deprecated: Entities that were already deprecated.
62
+ triples_added: Total triples added.
63
+ """
64
+
65
+ entities_deprecated: int = 0
66
+ entities_not_found: int = 0
67
+ entities_already_deprecated: int = 0
68
+ triples_added: int = 0
69
+
70
+
71
+ @dataclass
72
+ class DeprecationResult:
73
+ """Result of a deprecation operation.
74
+
75
+ Attributes:
76
+ deprecated_graph: The graph with deprecations added.
77
+ stats: Deprecation statistics.
78
+ success: Whether the operation succeeded.
79
+ error: Error message if success is False.
80
+ entity_info: Detailed info about each entity processed.
81
+ source_triples: Original triple count.
82
+ result_triples: Final triple count.
83
+ """
84
+
85
+ deprecated_graph: Graph | None = None
86
+ stats: DeprecationStats = field(default_factory=DeprecationStats)
87
+ success: bool = True
88
+ error: str | None = None
89
+ entity_info: list[EntityDeprecationInfo] = field(default_factory=list)
90
+ source_triples: int = 0
91
+ result_triples: int = 0
92
+
93
+
94
+ class OntologyDeprecator:
95
+ """Marks ontology entities as deprecated.
96
+
97
+ Adds standard OWL deprecation annotations:
98
+ - owl:deprecated true
99
+ - dcterms:isReplacedBy (if replacement specified)
100
+ - Prepends "DEPRECATED: " to rdfs:comment
101
+
102
+ Example usage:
103
+ deprecator = OntologyDeprecator()
104
+ result = deprecator.deprecate(
105
+ graph,
106
+ entity="http://example.org/OldClass",
107
+ replaced_by="http://example.org/NewClass",
108
+ message="Use NewClass instead."
109
+ )
110
+ """
111
+
112
+ def deprecate(
113
+ self,
114
+ graph: Graph,
115
+ entity: str,
116
+ replaced_by: str | None = None,
117
+ message: str | None = None,
118
+ version: str | None = None,
119
+ ) -> DeprecationResult:
120
+ """Mark a single entity as deprecated.
121
+
122
+ Args:
123
+ graph: Source RDF graph (will be modified in-place).
124
+ entity: URI of entity to deprecate.
125
+ replaced_by: Optional URI of replacement entity.
126
+ message: Optional deprecation message.
127
+ version: Optional version when deprecated.
128
+
129
+ Returns:
130
+ DeprecationResult with updated graph.
131
+ """
132
+ result = DeprecationResult()
133
+ result.source_triples = len(graph)
134
+
135
+ entity_uri = URIRef(entity)
136
+ info = EntityDeprecationInfo(uri=entity)
137
+
138
+ # Check if entity exists in the graph
139
+ entity_exists = False
140
+ for s, p, o in graph:
141
+ if s == entity_uri:
142
+ entity_exists = True
143
+ break
144
+ if o == entity_uri:
145
+ info.reference_count += 1
146
+
147
+ if not entity_exists:
148
+ # Entity not found as subject - check if it's referenced
149
+ info.found = False
150
+ result.stats.entities_not_found += 1
151
+ result.entity_info.append(info)
152
+ result.deprecated_graph = graph
153
+ result.result_triples = len(graph)
154
+ return result
155
+
156
+ # Get current labels and comments
157
+ for label in graph.objects(entity_uri, RDFS.label):
158
+ if isinstance(label, Literal):
159
+ info.current_labels.append(str(label))
160
+
161
+ for comment in graph.objects(entity_uri, RDFS.comment):
162
+ if isinstance(comment, Literal):
163
+ info.current_comments.append(str(comment))
164
+
165
+ # Check if already deprecated
166
+ for obj in graph.objects(entity_uri, OWL.deprecated):
167
+ if str(obj).lower() == "true":
168
+ info.was_already_deprecated = True
169
+ result.stats.entities_already_deprecated += 1
170
+ break
171
+
172
+ # Add owl:deprecated true (if not already present)
173
+ if not info.was_already_deprecated:
174
+ graph.add((entity_uri, OWL.deprecated, Literal(True)))
175
+ info.triples_added += 1
176
+ result.stats.entities_deprecated += 1
177
+
178
+ # Add dcterms:isReplacedBy if replacement specified
179
+ if replaced_by:
180
+ replaced_by_uri = URIRef(replaced_by)
181
+ # Remove any existing isReplacedBy
182
+ graph.remove((entity_uri, DCTERMS.isReplacedBy, None))
183
+ graph.add((entity_uri, DCTERMS.isReplacedBy, replaced_by_uri))
184
+ info.triples_added += 1
185
+ info.replaced_by = replaced_by
186
+
187
+ # Add/update deprecation comment
188
+ if message:
189
+ # Build full deprecation message
190
+ deprecation_msg = f"DEPRECATED: {message}"
191
+ if version:
192
+ deprecation_msg = f"DEPRECATED (v{version}): {message}"
193
+ info.message = deprecation_msg
194
+
195
+ # Check if there's an existing comment to update
196
+ existing_deprecated_comment = None
197
+ for comment in list(graph.objects(entity_uri, RDFS.comment)):
198
+ if isinstance(comment, Literal) and str(comment).startswith("DEPRECATED"):
199
+ existing_deprecated_comment = comment
200
+ break
201
+
202
+ if existing_deprecated_comment:
203
+ # Remove old deprecation comment
204
+ graph.remove((entity_uri, RDFS.comment, existing_deprecated_comment))
205
+
206
+ # Add new deprecation comment
207
+ graph.add((entity_uri, RDFS.comment, Literal(deprecation_msg, lang="en")))
208
+ info.triples_added += 1
209
+
210
+ # Ensure dcterms namespace is bound
211
+ graph.bind("dcterms", DCTERMS, override=False)
212
+
213
+ result.stats.triples_added += info.triples_added
214
+ result.entity_info.append(info)
215
+ result.deprecated_graph = graph
216
+ result.result_triples = len(graph)
217
+ result.success = True
218
+
219
+ return result
220
+
221
+ def deprecate_bulk(
222
+ self,
223
+ graph: Graph,
224
+ specs: list[DeprecationSpec],
225
+ ) -> DeprecationResult:
226
+ """Mark multiple entities as deprecated.
227
+
228
+ Args:
229
+ graph: Source RDF graph (will be modified in-place).
230
+ specs: List of deprecation specifications.
231
+
232
+ Returns:
233
+ DeprecationResult with updated graph.
234
+ """
235
+ combined_result = DeprecationResult()
236
+ combined_result.source_triples = len(graph)
237
+
238
+ for spec in specs:
239
+ result = self.deprecate(
240
+ graph=graph,
241
+ entity=spec.entity,
242
+ replaced_by=spec.replaced_by,
243
+ message=spec.message,
244
+ version=spec.version,
245
+ )
246
+
247
+ # Combine stats
248
+ combined_result.stats.entities_deprecated += result.stats.entities_deprecated
249
+ combined_result.stats.entities_not_found += result.stats.entities_not_found
250
+ combined_result.stats.entities_already_deprecated += (
251
+ result.stats.entities_already_deprecated
252
+ )
253
+ combined_result.stats.triples_added += result.stats.triples_added
254
+
255
+ # Combine entity info
256
+ combined_result.entity_info.extend(result.entity_info)
257
+
258
+ combined_result.deprecated_graph = graph
259
+ combined_result.result_triples = len(graph)
260
+ combined_result.success = True
261
+
262
+ return combined_result
263
+
264
+
265
+ def deprecate_file(
266
+ source_path: Path,
267
+ output_path: Path,
268
+ specs: list[DeprecationSpec],
269
+ ) -> DeprecationResult:
270
+ """Convenience function to deprecate entities in a file.
271
+
272
+ Args:
273
+ source_path: Path to source RDF file.
274
+ output_path: Path to write updated output.
275
+ specs: List of deprecation specifications.
276
+
277
+ Returns:
278
+ DeprecationResult with statistics.
279
+ """
280
+ # Load source graph
281
+ graph = Graph()
282
+ try:
283
+ graph.parse(source_path.as_posix())
284
+ except Exception as e:
285
+ result = DeprecationResult()
286
+ result.success = False
287
+ result.error = f"Failed to parse {source_path}: {e}"
288
+ return result
289
+
290
+ # Perform deprecation
291
+ deprecator = OntologyDeprecator()
292
+ result = deprecator.deprecate_bulk(graph, specs)
293
+
294
+ if not result.success:
295
+ return result
296
+
297
+ # Write output
298
+ if result.deprecated_graph:
299
+ output_path.parent.mkdir(parents=True, exist_ok=True)
300
+ result.deprecated_graph.serialize(destination=output_path.as_posix(), format="turtle")
301
+
302
+ return result
303
+
304
+
305
+ def generate_deprecation_message(
306
+ replaced_by: str | None,
307
+ message: str | None,
308
+ version: str | None,
309
+ ) -> str:
310
+ """Generate a standard deprecation message.
311
+
312
+ Args:
313
+ replaced_by: Replacement entity URI.
314
+ message: Custom message.
315
+ version: Version when deprecated.
316
+
317
+ Returns:
318
+ Formatted deprecation message.
319
+ """
320
+ if message:
321
+ return message
322
+
323
+ if replaced_by:
324
+ # Extract local name from URI
325
+ local_name = replaced_by.split("#")[-1].split("/")[-1]
326
+ return f"Use {local_name} instead."
327
+
328
+ return "This entity is deprecated and should not be used."
@@ -0,0 +1,8 @@
1
+ """Formatters for refactor command output.
2
+
3
+ This package provides formatters for dry-run previews and result output.
4
+ """
5
+
6
+ from rdf_construct.refactor.formatters.text import TextFormatter
7
+
8
+ __all__ = ["TextFormatter"]
@@ -0,0 +1,311 @@
1
+ """Text formatter for refactor command output.
2
+
3
+ Provides formatted output for dry-run previews and results.
4
+ """
5
+
6
+ from typing import Any
7
+
8
+ from rdf_construct.refactor.config import RenameConfig, RenameMapping, DeprecationSpec
9
+ from rdf_construct.refactor.renamer import RenameResult, RenameStats
10
+ from rdf_construct.refactor.deprecator import DeprecationResult, EntityDeprecationInfo
11
+
12
+
13
+ class TextFormatter:
14
+ """Text formatter for refactor results and previews.
15
+
16
+ Attributes:
17
+ use_colour: Whether to use ANSI colour codes.
18
+ """
19
+
20
+ def __init__(self, use_colour: bool = True):
21
+ """Initialize formatter.
22
+
23
+ Args:
24
+ use_colour: Whether to use ANSI colour codes.
25
+ """
26
+ self.use_colour = use_colour
27
+
28
+ def _colour(self, text: str, colour: str) -> str:
29
+ """Apply ANSI colour code if enabled.
30
+
31
+ Args:
32
+ text: Text to colour.
33
+ colour: Colour name (green, red, yellow, cyan, bold).
34
+
35
+ Returns:
36
+ Coloured text (or original if colour disabled).
37
+ """
38
+ if not self.use_colour:
39
+ return text
40
+
41
+ codes = {
42
+ "green": "\033[32m",
43
+ "red": "\033[31m",
44
+ "yellow": "\033[33m",
45
+ "cyan": "\033[36m",
46
+ "bold": "\033[1m",
47
+ "dim": "\033[2m",
48
+ "reset": "\033[0m",
49
+ }
50
+ return f"{codes.get(colour, '')}{text}{codes['reset']}"
51
+
52
+ def format_rename_preview(
53
+ self,
54
+ mappings: list[RenameMapping],
55
+ source_file: str,
56
+ source_triples: int,
57
+ literal_mentions: dict[str, int] | None = None,
58
+ ) -> str:
59
+ """Format a dry-run preview for rename operation.
60
+
61
+ Args:
62
+ mappings: List of rename mappings to apply.
63
+ source_file: Name of source file.
64
+ source_triples: Number of triples in source.
65
+ literal_mentions: Count of mentions in literals (won't be changed).
66
+
67
+ Returns:
68
+ Formatted preview string.
69
+ """
70
+ lines = [
71
+ self._colour("Refactoring Preview: Rename", "bold"),
72
+ "=" * 27,
73
+ "",
74
+ f"Source: {source_file} ({source_triples:,} triples)",
75
+ "",
76
+ ]
77
+
78
+ # Group by source type
79
+ namespace_mappings = [m for m in mappings if m.source == "namespace"]
80
+ explicit_mappings = [m for m in mappings if m.source == "explicit"]
81
+
82
+ if namespace_mappings:
83
+ # Group by namespace
84
+ namespaces: dict[str, list[RenameMapping]] = {}
85
+ for m in namespace_mappings:
86
+ # Extract namespace prefix
87
+ from_str = str(m.from_uri)
88
+ to_str = str(m.to_uri)
89
+ # Find common prefix
90
+ ns_from = from_str.rsplit("#", 1)[0] + "#" if "#" in from_str else from_str.rsplit("/", 1)[0] + "/"
91
+ ns_to = to_str.rsplit("#", 1)[0] + "#" if "#" in to_str else to_str.rsplit("/", 1)[0] + "/"
92
+ key = f"{ns_from} → {ns_to}"
93
+ if key not in namespaces:
94
+ namespaces[key] = []
95
+ namespaces[key].append(m)
96
+
97
+ lines.append(self._colour("Namespace renames:", "cyan"))
98
+ for ns_change, ns_mappings in namespaces.items():
99
+ lines.append(f" {ns_change}")
100
+ lines.append(f" - {len(ns_mappings)} entities affected")
101
+ lines.append("")
102
+
103
+ if explicit_mappings:
104
+ lines.append(self._colour("Entity renames:", "cyan"))
105
+ for m in explicit_mappings:
106
+ from_local = str(m.from_uri).split("#")[-1].split("/")[-1]
107
+ to_local = str(m.to_uri).split("#")[-1].split("/")[-1]
108
+ lines.append(f" {from_local} → {to_local}")
109
+
110
+ # Check for literal mentions
111
+ if literal_mentions and str(m.from_uri) in literal_mentions:
112
+ count = literal_mentions[str(m.from_uri)]
113
+ lines.append(
114
+ f" └─ {count} literal mention(s) "
115
+ f"{self._colour('(NOT changed)', 'yellow')}"
116
+ )
117
+ lines.append("")
118
+
119
+ # Summary
120
+ lines.append(self._colour("Totals:", "bold"))
121
+ lines.append(f" - {len(mappings)} entities to rename")
122
+ if namespace_mappings:
123
+ lines.append(f" - {len(namespace_mappings)} from namespace rules")
124
+ if explicit_mappings:
125
+ lines.append(f" - {len(explicit_mappings)} from explicit rules")
126
+ lines.append("")
127
+ lines.append(self._colour("Run without --dry-run to apply changes.", "dim"))
128
+
129
+ return "\n".join(lines)
130
+
131
+ def format_rename_result(self, result: RenameResult) -> str:
132
+ """Format the result of a rename operation.
133
+
134
+ Args:
135
+ result: Result from rename operation.
136
+
137
+ Returns:
138
+ Formatted result string.
139
+ """
140
+ if not result.success:
141
+ return self._colour(f"✗ Rename failed: {result.error}", "red")
142
+
143
+ lines = [
144
+ self._colour("Rename Complete", "green"),
145
+ "",
146
+ f" Source triples: {result.source_triples:,}",
147
+ f" Result triples: {result.result_triples:,}",
148
+ "",
149
+ self._colour("Changes:", "cyan"),
150
+ f" - Subjects renamed: {result.stats.subjects_renamed:,}",
151
+ f" - Predicates renamed: {result.stats.predicates_renamed:,}",
152
+ f" - Objects renamed: {result.stats.objects_renamed:,}",
153
+ f" - Total URI substitutions: {result.stats.total_renames:,}",
154
+ ]
155
+
156
+ if result.stats.namespace_entities > 0:
157
+ lines.append(f" - Via namespace rules: {result.stats.namespace_entities:,}")
158
+ if result.stats.explicit_entities > 0:
159
+ lines.append(f" - Via explicit rules: {result.stats.explicit_entities:,}")
160
+
161
+ if result.stats.literal_mentions:
162
+ lines.append("")
163
+ lines.append(
164
+ self._colour(
165
+ f" ⚠ {len(result.stats.literal_mentions)} entities mentioned in literals "
166
+ "(not modified)",
167
+ "yellow",
168
+ )
169
+ )
170
+
171
+ return "\n".join(lines)
172
+
173
+ def format_deprecation_preview(
174
+ self,
175
+ specs: list[DeprecationSpec],
176
+ entity_info: list[EntityDeprecationInfo] | None = None,
177
+ source_file: str = "",
178
+ source_triples: int = 0,
179
+ ) -> str:
180
+ """Format a dry-run preview for deprecation operation.
181
+
182
+ Args:
183
+ specs: List of deprecation specifications.
184
+ entity_info: Optional entity information from dry run.
185
+ source_file: Name of source file.
186
+ source_triples: Number of triples in source.
187
+
188
+ Returns:
189
+ Formatted preview string.
190
+ """
191
+ lines = [
192
+ self._colour("Refactoring Preview: Deprecate", "bold"),
193
+ "=" * 30,
194
+ "",
195
+ f"Source: {source_file} ({source_triples:,} triples)" if source_file else "",
196
+ "",
197
+ self._colour("Entities to deprecate:", "cyan"),
198
+ "",
199
+ ]
200
+
201
+ for i, spec in enumerate(specs):
202
+ # Extract local name
203
+ local_name = spec.entity.split("#")[-1].split("/")[-1]
204
+ lines.append(f" {self._colour(local_name, 'bold')}")
205
+
206
+ # Show current state if available
207
+ if entity_info and i < len(entity_info):
208
+ info = entity_info[i]
209
+ if not info.found:
210
+ lines.append(f" {self._colour('⚠ Entity not found in graph', 'yellow')}")
211
+ else:
212
+ if info.was_already_deprecated:
213
+ lines.append(f" {self._colour('Already deprecated', 'yellow')}")
214
+ if info.current_labels:
215
+ lines.append(f" rdfs:label: \"{info.current_labels[0]}\"")
216
+ if info.reference_count > 0:
217
+ lines.append(
218
+ f" {self._colour(f'Referenced {info.reference_count} times', 'dim')}"
219
+ )
220
+
221
+ # Show what will be added
222
+ lines.append(" Will add:")
223
+ lines.append(f" owl:deprecated {self._colour('true', 'green')}")
224
+
225
+ if spec.replaced_by:
226
+ repl_local = spec.replaced_by.split("#")[-1].split("/")[-1]
227
+ lines.append(f" dcterms:isReplacedBy {self._colour(repl_local, 'cyan')}")
228
+
229
+ if spec.message:
230
+ msg_preview = spec.message[:50] + "..." if len(spec.message) > 50 else spec.message
231
+ lines.append(f" rdfs:comment \"DEPRECATED: {msg_preview}\"")
232
+
233
+ lines.append("")
234
+
235
+ # Summary
236
+ lines.append(self._colour("Summary:", "bold"))
237
+ lines.append(f" - {len(specs)} entities will be marked deprecated")
238
+ with_replacement = len([s for s in specs if s.replaced_by])
239
+ lines.append(f" - {with_replacement} with replacement")
240
+ lines.append(f" - {len(specs) - with_replacement} without replacement")
241
+ lines.append("")
242
+ lines.append(
243
+ self._colour(
244
+ "Note: Deprecation marks entities but does not rename or migrate.",
245
+ "dim",
246
+ )
247
+ )
248
+ lines.append(
249
+ self._colour(
250
+ " Use 'refactor rename' to actually migrate references.",
251
+ "dim",
252
+ )
253
+ )
254
+ lines.append("")
255
+ lines.append(self._colour("Run without --dry-run to apply changes.", "dim"))
256
+
257
+ return "\n".join(lines)
258
+
259
+ def format_deprecation_result(self, result: DeprecationResult) -> str:
260
+ """Format the result of a deprecation operation.
261
+
262
+ Args:
263
+ result: Result from deprecation operation.
264
+
265
+ Returns:
266
+ Formatted result string.
267
+ """
268
+ if not result.success:
269
+ return self._colour(f"✗ Deprecation failed: {result.error}", "red")
270
+
271
+ lines = [
272
+ self._colour("Deprecation Complete", "green"),
273
+ "",
274
+ f" Source triples: {result.source_triples:,}",
275
+ f" Result triples: {result.result_triples:,}",
276
+ "",
277
+ self._colour("Changes:", "cyan"),
278
+ f" - Entities deprecated: {result.stats.entities_deprecated}",
279
+ f" - Triples added: {result.stats.triples_added}",
280
+ ]
281
+
282
+ if result.stats.entities_not_found > 0:
283
+ lines.append(
284
+ self._colour(
285
+ f" ⚠ Entities not found: {result.stats.entities_not_found}",
286
+ "yellow",
287
+ )
288
+ )
289
+
290
+ if result.stats.entities_already_deprecated > 0:
291
+ lines.append(
292
+ f" - Already deprecated: {result.stats.entities_already_deprecated}"
293
+ )
294
+
295
+ # Show details of deprecated entities
296
+ if result.entity_info:
297
+ lines.append("")
298
+ lines.append("Details:")
299
+ for info in result.entity_info:
300
+ local_name = info.uri.split("#")[-1].split("/")[-1]
301
+ if not info.found:
302
+ lines.append(f" {self._colour('⚠', 'yellow')} {local_name} - not found")
303
+ elif info.was_already_deprecated and info.triples_added == 0:
304
+ lines.append(f" ○ {local_name} - already deprecated, no changes")
305
+ else:
306
+ lines.append(f" {self._colour('✓', 'green')} {local_name}")
307
+ if info.replaced_by:
308
+ repl_local = info.replaced_by.split("#")[-1].split("/")[-1]
309
+ lines.append(f" → replaced by {repl_local}")
310
+
311
+ return "\n".join(lines)