issues-fs 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.
Files changed (77) hide show
  1. issues_fs/issues/Issue__Path__Config.py +25 -0
  2. issues_fs/issues/__init__.py +3 -0
  3. issues_fs/issues/graph_services/Comments__Service.py +235 -0
  4. issues_fs/issues/graph_services/Graph__Repository.py +307 -0
  5. issues_fs/issues/graph_services/Graph__Repository__Factory.py +106 -0
  6. issues_fs/issues/graph_services/Link__Service.py +227 -0
  7. issues_fs/issues/graph_services/Node__Service.py +392 -0
  8. issues_fs/issues/graph_services/Type__Service.py +209 -0
  9. issues_fs/issues/graph_services/__init__.py +3 -0
  10. issues_fs/issues/phase_1/Issue__Children__Service.py +340 -0
  11. issues_fs/issues/phase_1/Root__Issue__Service.py +137 -0
  12. issues_fs/issues/phase_1/Root__Selection__Service.py +310 -0
  13. issues_fs/issues/phase_1/__init__.py +3 -0
  14. issues_fs/issues/status/Git__Status__Service.py +115 -0
  15. issues_fs/issues/status/Index__Status__Service.py +131 -0
  16. issues_fs/issues/status/Server__Status__Service.py +113 -0
  17. issues_fs/issues/status/Storage__Status__Service.py +75 -0
  18. issues_fs/issues/status/Types__Status__Service.py +75 -0
  19. issues_fs/issues/status/__init__.py +3 -0
  20. issues_fs/issues/storage/Path__Handler__Graph_Node.py +185 -0
  21. issues_fs/issues/storage/Path__Handler__Issues.py +57 -0
  22. issues_fs/issues/storage/__init__.py +3 -0
  23. issues_fs/schemas/__init__.py +0 -0
  24. issues_fs/schemas/enums/Enum__Comment__Author.py +10 -0
  25. issues_fs/schemas/enums/Enum__Graph__Storage__Backend.py +8 -0
  26. issues_fs/schemas/enums/Enum__Issue__Status.py +13 -0
  27. issues_fs/schemas/enums/__init__.py +3 -0
  28. issues_fs/schemas/graph/Safe_Str__Graph_Types.py +63 -0
  29. issues_fs/schemas/graph/Schema__Global__Index.py +16 -0
  30. issues_fs/schemas/graph/Schema__Graph__Link.py +8 -0
  31. issues_fs/schemas/graph/Schema__Graph__Node.py +10 -0
  32. issues_fs/schemas/graph/Schema__Graph__Response.py +21 -0
  33. issues_fs/schemas/graph/Schema__Link__Create__Request.py +12 -0
  34. issues_fs/schemas/graph/Schema__Link__Create__Response.py +15 -0
  35. issues_fs/schemas/graph/Schema__Link__Delete__Response.py +16 -0
  36. issues_fs/schemas/graph/Schema__Link__List__Response.py +15 -0
  37. issues_fs/schemas/graph/Schema__Link__Type.py +20 -0
  38. issues_fs/schemas/graph/Schema__Node.py +43 -0
  39. issues_fs/schemas/graph/Schema__Node__Create__Request.py +19 -0
  40. issues_fs/schemas/graph/Schema__Node__Create__Response.py +14 -0
  41. issues_fs/schemas/graph/Schema__Node__Delete__Response.py +15 -0
  42. issues_fs/schemas/graph/Schema__Node__Link.py +17 -0
  43. issues_fs/schemas/graph/Schema__Node__List__Response.py +16 -0
  44. issues_fs/schemas/graph/Schema__Node__Summary.py +15 -0
  45. issues_fs/schemas/graph/Schema__Node__Type.py +28 -0
  46. issues_fs/schemas/graph/Schema__Node__Update__Request.py +18 -0
  47. issues_fs/schemas/graph/Schema__Node__Update__Response.py +14 -0
  48. issues_fs/schemas/graph/Schema__Property__Definition.py +27 -0
  49. issues_fs/schemas/graph/Schema__Type__Index.py +16 -0
  50. issues_fs/schemas/graph/Schema__Type__Summary.py +13 -0
  51. issues_fs/schemas/graph/__init__.py +0 -0
  52. issues_fs/schemas/identifiers/Comment_Id.py +5 -0
  53. issues_fs/schemas/identifiers/Issue_Id.py +13 -0
  54. issues_fs/schemas/identifiers/__init__.py +0 -0
  55. issues_fs/schemas/issues/Schema__Comment.py +61 -0
  56. issues_fs/schemas/issues/__init__.py +0 -0
  57. issues_fs/schemas/issues/phase_1/Schema__Issue__Children.py +85 -0
  58. issues_fs/schemas/issues/phase_1/Schema__Root.py +60 -0
  59. issues_fs/schemas/issues/phase_1/__init__.py +0 -0
  60. issues_fs/schemas/safe_str/Safe_Str__Hex_Color.py +10 -0
  61. issues_fs/schemas/safe_str/Safe_Str__Issue_Id.py +14 -0
  62. issues_fs/schemas/safe_str/Safe_Str__Issue__Node__Description.py +15 -0
  63. issues_fs/schemas/safe_str/Safe_Str__Label_Name.py +9 -0
  64. issues_fs/schemas/safe_str/__init__.py +0 -0
  65. issues_fs/schemas/status/Schema__API__Info.py +15 -0
  66. issues_fs/schemas/status/Schema__Git__Status.py +21 -0
  67. issues_fs/schemas/status/Schema__Index__Status.py +22 -0
  68. issues_fs/schemas/status/Schema__Server__Status.py +28 -0
  69. issues_fs/schemas/status/Schema__Storage__Status.py +23 -0
  70. issues_fs/schemas/status/Schema__Types__Status.py +30 -0
  71. issues_fs/schemas/status/__init__.py +0 -0
  72. issues_fs/version +1 -1
  73. {issues_fs-0.3.0.dist-info → issues_fs-0.4.0.dist-info}/METADATA +2 -1
  74. issues_fs-0.4.0.dist-info/RECORD +79 -0
  75. issues_fs-0.3.0.dist-info/RECORD +0 -8
  76. {issues_fs-0.3.0.dist-info → issues_fs-0.4.0.dist-info}/LICENSE +0 -0
  77. {issues_fs-0.3.0.dist-info → issues_fs-0.4.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,392 @@
1
+ # ═══════════════════════════════════════════════════════════════════════════════
2
+ # Node__Service - Business logic for node operations
3
+ # Handles create, update, delete, and query operations for graph nodes
4
+ # ═══════════════════════════════════════════════════════════════════════════════
5
+
6
+ from typing import List, Optional
7
+ from osbot_utils.type_safe.Type_Safe import Type_Safe
8
+ from osbot_utils.type_safe.primitives.core.Safe_UInt import Safe_UInt
9
+ from osbot_utils.type_safe.primitives.domains.identifiers.Obj_Id import Obj_Id
10
+ from osbot_utils.type_safe.primitives.domains.identifiers.safe_int.Timestamp_Now import Timestamp_Now
11
+ from issues_fs.schemas.graph.Safe_Str__Graph_Types import Safe_Str__Node_Type, Safe_Str__Node_Label
12
+ from issues_fs.schemas.graph.Schema__Global__Index import Schema__Global__Index
13
+ from issues_fs.schemas.graph.Schema__Graph__Link import Schema__Graph__Link
14
+ from issues_fs.schemas.graph.Schema__Graph__Node import Schema__Graph__Node
15
+ from issues_fs.schemas.graph.Schema__Graph__Response import Schema__Graph__Response
16
+ from issues_fs.schemas.graph.Schema__Node import Schema__Node
17
+ from issues_fs.schemas.graph.Schema__Node__Create__Request import Schema__Node__Create__Request
18
+ from issues_fs.schemas.graph.Schema__Node__Create__Response import Schema__Node__Create__Response
19
+ from issues_fs.schemas.graph.Schema__Node__Delete__Response import Schema__Node__Delete__Response
20
+ from issues_fs.schemas.graph.Schema__Node__Link import Schema__Node__Link
21
+ from issues_fs.schemas.graph.Schema__Node__List__Response import Schema__Node__List__Response
22
+ from issues_fs.schemas.graph.Schema__Node__Summary import Schema__Node__Summary
23
+ from issues_fs.schemas.graph.Schema__Node__Update__Request import Schema__Node__Update__Request
24
+ from issues_fs.schemas.graph.Schema__Node__Update__Response import Schema__Node__Update__Response
25
+ from issues_fs.schemas.graph.Schema__Type__Summary import Schema__Type__Summary
26
+ from issues_fs.issues.graph_services.Graph__Repository import Graph__Repository
27
+
28
+ # todo: refactor to Issue__Node__Service
29
+ class Node__Service(Type_Safe): # Node business logic service
30
+ repository : Graph__Repository # Data access layer
31
+
32
+ # ═══════════════════════════════════════════════════════════════════════════════
33
+ # Query Operations
34
+ # ═══════════════════════════════════════════════════════════════════════════════
35
+
36
+ def get_node(self , # Get single node by label
37
+ node_type : Safe_Str__Node_Type ,
38
+ label : Safe_Str__Node_Label
39
+ ) -> Optional[Schema__Node]:
40
+ return self.repository.node_load(node_type = node_type ,
41
+ label = label )
42
+
43
+ def node_exists(self , # Check if node exists
44
+ node_type : Safe_Str__Node_Type ,
45
+ label : Safe_Str__Node_Label
46
+ ) -> bool:
47
+ return self.repository.node_exists(node_type = node_type ,
48
+ label = label )
49
+
50
+ def list_nodes(self , # List nodes, optionally filtered by type
51
+ node_type : Safe_Str__Node_Type = None
52
+ ) -> Schema__Node__List__Response:
53
+ summaries = []
54
+
55
+ if node_type: # List nodes of specific type
56
+ summaries = self.list_nodes_for_type(node_type)
57
+ else: # List all nodes across all types
58
+ node_types = self.repository.node_types_load()
59
+ for nt in node_types:
60
+ type_summaries = self.list_nodes_for_type(nt.name)
61
+ summaries.extend(type_summaries)
62
+
63
+ return Schema__Node__List__Response(success = True ,
64
+ nodes = summaries ,
65
+ total = len(summaries) )
66
+
67
+ def list_nodes_for_type(self , # List nodes for a specific type
68
+ node_type : Safe_Str__Node_Type
69
+ ) -> List[Schema__Node__Summary]:
70
+ summaries = []
71
+ labels = self.repository.nodes_list_labels(node_type)
72
+
73
+ for label in labels:
74
+ node = self.repository.node_load(node_type = node_type ,
75
+ label = label )
76
+ if node:
77
+ summary = Schema__Node__Summary(label = node.label ,
78
+ node_type = node.node_type ,
79
+ title = node.title ,
80
+ status = node.status )
81
+ summaries.append(summary)
82
+
83
+ return summaries
84
+
85
+ # ═══════════════════════════════════════════════════════════════════════════════
86
+ # Create Operations
87
+ # ═══════════════════════════════════════════════════════════════════════════════
88
+
89
+ def create_node(self , # Create new node
90
+ request : Schema__Node__Create__Request
91
+ ) -> Schema__Node__Create__Response:
92
+ # Validate title is not empty
93
+ if str(request.title).strip() == '':
94
+ return Schema__Node__Create__Response(success = False ,
95
+ message = 'Title is required' )
96
+
97
+ # Validate node type exists
98
+ node_types = self.repository.node_types_load()
99
+ node_type_def = None
100
+ for nt in node_types:
101
+ if str(nt.name) == str(request.node_type):
102
+ node_type_def = nt
103
+ break
104
+
105
+ if node_type_def is None:
106
+ return Schema__Node__Create__Response(success = False ,
107
+ message = f'Unknown node type: {request.node_type}')
108
+
109
+ # Get next index for this type
110
+ type_index = self.repository.type_index_load(request.node_type)
111
+ next_num = int(type_index.next_index)
112
+ label = self.label_from_type_and_index(request.node_type, next_num)
113
+
114
+ # SAFETY CHECK: If label already exists, find actual next available index
115
+ while self.repository.node_exists(node_type=request.node_type, label=label):
116
+ next_num += 1
117
+ label = self.label_from_type_and_index(request.node_type, next_num)
118
+
119
+ now = Timestamp_Now()
120
+
121
+ # Determine status
122
+ status = request.status if request.status else node_type_def.default_status
123
+
124
+ # Create node
125
+ node = Schema__Node(node_id = Obj_Id() ,
126
+ node_type = request.node_type ,
127
+ node_index = Safe_UInt(next_num) ,
128
+ label = label ,
129
+ title = request.title ,
130
+ description = request.description ,
131
+ status = status ,
132
+ created_at = now ,
133
+ updated_at = now ,
134
+ created_by = Obj_Id() , # TODO: actual creator
135
+ tags = list(request.tags) if request.tags else [],
136
+ links = [] ,
137
+ properties = dict(request.properties) if request.properties else {})
138
+
139
+ # Save node
140
+ if self.repository.node_save(node) is False:
141
+ return Schema__Node__Create__Response(success = False ,
142
+ message = 'Failed to save node')
143
+
144
+ # Update type index
145
+ type_index.next_index = Safe_UInt(next_num + 1)
146
+ type_index.count = Safe_UInt(int(type_index.count) + 1)
147
+ type_index.last_updated = now
148
+ self.repository.type_index_save(type_index)
149
+
150
+ # Update global index
151
+ self.update_global_index()
152
+
153
+ return Schema__Node__Create__Response(success = True ,
154
+ node = node )
155
+
156
+ # ═══════════════════════════════════════════════════════════════════════════════
157
+ # Update Operations
158
+ # ═══════════════════════════════════════════════════════════════════════════════
159
+
160
+ def update_node(self , # Update existing node
161
+ node_type : Safe_Str__Node_Type ,
162
+ label : Safe_Str__Node_Label ,
163
+ request : Schema__Node__Update__Request
164
+ ) -> Schema__Node__Update__Response:
165
+ node = self.repository.node_load(node_type = node_type ,
166
+ label = label )
167
+ if node is None:
168
+ return Schema__Node__Update__Response(success = False ,
169
+ message = f'Node not found: {label}')
170
+
171
+ # Apply updates - use truthiness check because Type_Safe auto-initializes
172
+ # empty strings for Safe_Str types (so `is not None` doesn't work)
173
+ if request.title: # Only update if non-empty
174
+ node.title = request.title
175
+ if request.description: # Only update if non-empty
176
+ node.description = request.description
177
+ if request.status: # Only update if non-empty
178
+ node.status = request.status
179
+ if request.tags is not None: # Tags can be empty list
180
+ node.tags = list(request.tags)
181
+ if request.properties is not None: # Deep merge properties
182
+ node.properties = self.deep_merge_properties(node.properties, request.properties)
183
+
184
+ node.updated_at = Timestamp_Now()
185
+
186
+ # Save
187
+ if self.repository.node_save(node) is False:
188
+ return Schema__Node__Update__Response(success = False ,
189
+ message = 'Failed to save node' )
190
+
191
+ return Schema__Node__Update__Response(success = True ,
192
+ node = node )
193
+
194
+ def deep_merge_properties(self , # Deep merge properties dicts
195
+ existing : dict ,
196
+ updates : dict
197
+ ) -> dict:
198
+ result = dict(existing) if existing else {} # Copy existing properties
199
+
200
+ for key, value in updates.items():
201
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
202
+ result[key] = self.deep_merge_properties(result[key], value) # Recursively merge nested dicts
203
+ else:
204
+ result[key] = value # Replace or add key
205
+
206
+ return result
207
+
208
+ # ═══════════════════════════════════════════════════════════════════════════════
209
+ # Delete Operations
210
+ # ═══════════════════════════════════════════════════════════════════════════════
211
+
212
+ def delete_node(self , # Delete node
213
+ node_type : Safe_Str__Node_Type ,
214
+ label : Safe_Str__Node_Label
215
+ ) -> Schema__Node__Delete__Response:
216
+ if self.repository.node_exists(node_type, label) is False:
217
+ return Schema__Node__Delete__Response(success = False ,
218
+ deleted = False ,
219
+ label = label ,
220
+ message = f'Node not found: {label}')
221
+
222
+ # TODO: Remove links from other nodes pointing to this one
223
+
224
+ # Delete node
225
+ if self.repository.node_delete(node_type, label) is False:
226
+ return Schema__Node__Delete__Response(success = False ,
227
+ deleted = False ,
228
+ label = label ,
229
+ message = 'Failed to delete node' )
230
+
231
+ # Update type index
232
+ type_index = self.repository.type_index_load(node_type)
233
+ type_index.count = Safe_UInt(max(0, int(type_index.count) - 1))
234
+ type_index.last_updated = Timestamp_Now()
235
+ self.repository.type_index_save(type_index)
236
+
237
+ # Update global index
238
+ self.update_global_index()
239
+
240
+ return Schema__Node__Delete__Response(success = True ,
241
+ deleted = True ,
242
+ label = label )
243
+
244
+ # ═══════════════════════════════════════════════════════════════════════════════
245
+ # Helper Methods
246
+ # ═══════════════════════════════════════════════════════════════════════════════
247
+
248
+ def label_from_type_and_index(self , # Generate label
249
+ node_type : Safe_Str__Node_Type ,
250
+ node_index : int
251
+ ) -> Safe_Str__Node_Label:
252
+ display_type = str(node_type).capitalize()
253
+ return Safe_Str__Node_Label(f"{display_type}-{node_index}")
254
+
255
+ def update_global_index(self) -> None: # Recalculate global index
256
+ node_types = self.repository.node_types_load()
257
+ total_nodes = 0
258
+ type_counts = []
259
+
260
+ for nt in node_types:
261
+ type_index = self.repository.type_index_load(nt.name)
262
+ count = int(type_index.count)
263
+ total_nodes += count
264
+ type_counts.append(Schema__Type__Summary(node_type = nt.name ,
265
+ count = Safe_UInt(count) ))
266
+
267
+ global_index = Schema__Global__Index(total_nodes = Safe_UInt(total_nodes) ,
268
+ last_updated = Timestamp_Now() ,
269
+ type_counts = type_counts )
270
+
271
+ self.repository.global_index_save(global_index)
272
+
273
+ # ═══════════════════════════════════════════════════════════════════════════════
274
+ # Graph Traversal Operations
275
+ # ═══════════════════════════════════════════════════════════════════════════════
276
+
277
+ def get_node_graph(self , # Get node with connected nodes
278
+ node_type : Safe_Str__Node_Type ,
279
+ label : Safe_Str__Node_Label ,
280
+ depth : int = 1
281
+ ) -> 'Schema__Graph__Response':
282
+
283
+ if depth > 3: # Cap depth to prevent expensive traversals
284
+ depth = 3
285
+
286
+ root_node = self.repository.node_load(node_type = node_type ,
287
+ label = label )
288
+ if root_node is None:
289
+ return Schema__Graph__Response(success = False ,
290
+ root = label ,
291
+ nodes = [] ,
292
+ links = [] ,
293
+ depth = depth ,
294
+ message = f'Node not found: {label}')
295
+
296
+ visited_labels = set()
297
+ nodes = []
298
+ links = []
299
+
300
+ self._traverse_graph(root_node, depth, visited_labels, nodes, links)
301
+
302
+ return Schema__Graph__Response(success = True ,
303
+ root = label ,
304
+ nodes = nodes ,
305
+ links = links ,
306
+ depth = depth )
307
+
308
+ def _traverse_graph(self , # Recursively traverse graph
309
+ node : Schema__Node ,
310
+ depth : int ,
311
+ visited : set ,
312
+ nodes : list ,
313
+ links : list
314
+ ) -> None:
315
+
316
+ label_str = str(node.label)
317
+ if label_str in visited or depth < 0:
318
+ return
319
+
320
+ visited.add(label_str)
321
+ nodes.append(Schema__Graph__Node(label = node.label ,
322
+ title = node.title ,
323
+ node_type = node.node_type ,
324
+ status = node.status ))
325
+
326
+ if depth == 0:
327
+ return
328
+
329
+ # Traverse outgoing links
330
+ if node.links:
331
+ for link in node.links:
332
+ target_label = link.target_label
333
+ if target_label and str(target_label) not in visited:
334
+ target_node = self._resolve_link_target(link)
335
+ if target_node:
336
+ links.append(Schema__Graph__Link(source = node.label ,
337
+ target = target_node.label,
338
+ link_type = link.verb))
339
+ self._traverse_graph(target_node, depth - 1, visited, nodes, links)
340
+
341
+ # Find and traverse incoming links
342
+ incoming = self._find_incoming_links(node.label)
343
+ for source_node, link_type in incoming:
344
+ if str(source_node.label) not in visited:
345
+ links.append(Schema__Graph__Link(source = source_node.label ,
346
+ target = node.label ,
347
+ link_type = link_type ))
348
+ self._traverse_graph(source_node, depth - 1, visited, nodes, links)
349
+
350
+ def _resolve_link_target(self , # Load target node from link
351
+ link : Schema__Node__Link
352
+ ) -> Schema__Node:
353
+ if not link.target_label:
354
+ return None
355
+
356
+ target_label = str(link.target_label)
357
+ parts = target_label.split('-') # Parse "Bug-1" -> type="bug", label="Bug-1"
358
+
359
+ if len(parts) != 2:
360
+ return None
361
+
362
+ target_type = parts[0].lower() # "Bug" -> "bug"
363
+
364
+ try:
365
+ return self.repository.node_load(node_type = Safe_Str__Node_Type(target_type) ,
366
+ label = Safe_Str__Node_Label(target_label))
367
+ except Exception:
368
+ return None
369
+
370
+ # todo: this should not be a tuple, this should be a Type_Safe class
371
+ def _find_incoming_links(self , # Find nodes that link TO this node
372
+ label : Safe_Str__Node_Label
373
+ ) -> List[tuple]:
374
+ incoming = []
375
+ label_str = str(label)
376
+ node_types = self.repository.node_types_load()
377
+
378
+ for nt in node_types:
379
+ labels = self.repository.nodes_list_labels(nt.name)
380
+ for node_label in labels:
381
+ if str(node_label) == label_str: # Skip self
382
+ continue
383
+
384
+ node = self.repository.node_load(node_type = nt.name ,
385
+ label = node_label)
386
+ if node and node.links:
387
+ for link in node.links:
388
+ if link.target_label and str(link.target_label) == label_str:
389
+ incoming.append((node, str(link.verb)))
390
+ break # Only add node once
391
+
392
+ return incoming
@@ -0,0 +1,209 @@
1
+ # ═══════════════════════════════════════════════════════════════════════════════
2
+ # Type__Service - Business logic for type definitions
3
+ # Manages node types (bug, task, feature) and link types (blocks, has-task)
4
+ # Phase 1: Added git-repo type for root issue support
5
+ # ═══════════════════════════════════════════════════════════════════════════════
6
+
7
+ from typing import List, Optional
8
+ from osbot_utils.type_safe.Type_Safe import Type_Safe
9
+ from osbot_utils.type_safe.primitives.domains.common.safe_str.Safe_Str__Text import Safe_Str__Text
10
+ from osbot_utils.type_safe.primitives.domains.identifiers.Obj_Id import Obj_Id
11
+ from osbot_utils.type_safe.type_safe_core.decorators.type_safe import type_safe
12
+
13
+ from issues_fs.schemas.graph.Safe_Str__Graph_Types import Safe_Str__Node_Type, Safe_Str__Status, Safe_Str__Node_Type_Display, Safe_Str__Link_Verb
14
+ from issues_fs.schemas.graph.Schema__Node__Type import Schema__Node__Type
15
+ from issues_fs.schemas.graph.Schema__Link__Type import Schema__Link__Type
16
+ from issues_fs.schemas.safe_str.Safe_Str__Hex_Color import Safe_Str__Hex_Color
17
+ from issues_fs.issues.graph_services.Graph__Repository import Graph__Repository
18
+
19
+
20
+ class Type__Service(Type_Safe): # Type definition service
21
+ repository : Graph__Repository # Data access layer
22
+
23
+ # ═══════════════════════════════════════════════════════════════════════════════
24
+ # Node Type Operations
25
+ # ═══════════════════════════════════════════════════════════════════════════════
26
+
27
+ @type_safe
28
+ def list_node_types(self) -> List[Schema__Node__Type]: # Get all node types
29
+ return self.repository.node_types_load()
30
+
31
+ def get_node_type(self , # Get single node type
32
+ name : Safe_Str__Node_Type
33
+ ) -> Schema__Node__Type:
34
+ types = self.repository.node_types_load()
35
+ for t in types:
36
+ if str(t.name) == str(name):
37
+ return t
38
+ return None
39
+
40
+ @type_safe
41
+ def create_node_type(self , # Create new node type
42
+ name : Safe_Str__Node_Type ,
43
+ display_name : Safe_Str__Node_Type_Display ,
44
+ description : Safe_Str__Text = '' ,
45
+ color : Safe_Str__Hex_Color = '#888888',
46
+ statuses : List[str] = None ,
47
+ default_status : Safe_Str__Status = 'backlog'
48
+ ) -> Schema__Node__Type:
49
+ types = self.repository.node_types_load()
50
+
51
+ for t in types:
52
+ if t.name == name:
53
+ return None # Already exists
54
+
55
+ status_list = statuses or ['backlog', 'in-progress', 'done']
56
+
57
+ node_type = Schema__Node__Type(type_id = Obj_Id() ,
58
+ name = name ,
59
+ display_name = display_name ,
60
+ description = description ,
61
+ color = color ,
62
+ statuses = status_list ,
63
+ default_status = default_status )
64
+
65
+ types.append(node_type)
66
+ self.repository.node_types_save(types)
67
+ return node_type
68
+
69
+ def delete_node_type(self , # Delete node type
70
+ name : Safe_Str__Node_Type
71
+ ) -> bool:
72
+ types = self.repository.node_types_load()
73
+
74
+ type_index = self.repository.type_index_load(name) # Check if any nodes of this type exist
75
+ if int(type_index.count) > 0:
76
+ return False # Cannot delete type with existing nodes
77
+
78
+ types = [t for t in types if str(t.name) != str(name)] # Remove type
79
+ self.repository.node_types_save(types)
80
+ return True
81
+
82
+ # ═══════════════════════════════════════════════════════════════════════════════
83
+ # Link Type Operations
84
+ # ═══════════════════════════════════════════════════════════════════════════════
85
+
86
+ @type_safe
87
+ def list_link_types(self) -> List[Schema__Link__Type]: # Get all link types
88
+ return self.repository.link_types_load()
89
+
90
+ def get_link_type(self , # Get link type by verb
91
+ verb : Safe_Str__Link_Verb
92
+ ) -> Optional[Schema__Link__Type]:
93
+ types = self.repository.link_types_load()
94
+ for t in types:
95
+ if str(t.verb) == str(verb):
96
+ return t
97
+ return None
98
+
99
+ @type_safe
100
+ def create_link_type(self , # Create new link type
101
+ verb : Safe_Str__Link_Verb ,
102
+ inverse_verb : Safe_Str__Link_Verb ,
103
+ description : Safe_Str__Text = '' ,
104
+ source_types : List[Safe_Str__Node_Type] = None ,
105
+ target_types : List[Safe_Str__Node_Type] = None
106
+ ) -> Schema__Link__Type:
107
+ types = self.repository.link_types_load()
108
+
109
+ for t in types:
110
+ if str(t.verb) == str(verb):
111
+ return None # Already exists
112
+
113
+ link_type = Schema__Link__Type(link_type_id = Obj_Id() ,
114
+ verb = verb ,
115
+ inverse_verb = inverse_verb ,
116
+ description = description ,
117
+ source_types = source_types ,
118
+ target_types = target_types )
119
+
120
+ types.append(link_type)
121
+ self.repository.link_types_save(types)
122
+ return link_type
123
+
124
+ # ═══════════════════════════════════════════════════════════════════════════════
125
+ # Default Type Initialization
126
+ # ═══════════════════════════════════════════════════════════════════════════════
127
+
128
+ def initialize_default_types(self) -> None: # Set up default types
129
+ if len(self.repository.node_types_load()) > 0: # Check if already initialized
130
+ return
131
+
132
+ # ───────────────────────────────────────────────────────────────────────────
133
+ # Node Types
134
+ # ───────────────────────────────────────────────────────────────────────────
135
+
136
+ self.create_node_type(name = Safe_Str__Node_Type('git-repo') , # NEW: Root issue type for git repositories
137
+ display_name = 'GitRepo' ,
138
+ description = 'Git repository root - contains all issues',
139
+ color = '#6366f1' , # Indigo color
140
+ statuses = ['active', 'archived'] ,
141
+ default_status = 'active' )
142
+
143
+ self.create_node_type(name = Safe_Str__Node_Type('bug') ,
144
+ display_name = 'Bug' ,
145
+ description = 'Defect or error in the system' ,
146
+ color = '#ef4444' ,
147
+ statuses = ['backlog', 'confirmed', 'in-progress', 'testing', 'resolved', 'closed'],
148
+ default_status = 'backlog' )
149
+
150
+ self.create_node_type(name = Safe_Str__Node_Type('task') ,
151
+ display_name = 'Task' ,
152
+ description = 'Unit of work to be completed' ,
153
+ color = '#3b82f6' ,
154
+ statuses = ['backlog', 'todo', 'in-progress', 'review', 'done'],
155
+ default_status = 'backlog' )
156
+
157
+ self.create_node_type(name = Safe_Str__Node_Type('feature') ,
158
+ display_name = 'Feature' ,
159
+ description = 'High-level capability' ,
160
+ color = '#22c55e' ,
161
+ statuses = ['proposed', 'approved', 'in-progress', 'released'],
162
+ default_status = 'proposed' )
163
+
164
+ self.create_node_type(name = Safe_Str__Node_Type('person') ,
165
+ display_name = 'Person' ,
166
+ description = 'Human or agent identity' ,
167
+ color = '#8b5cf6' ,
168
+ statuses = ['active', 'inactive'] ,
169
+ default_status = 'active' )
170
+
171
+ # ───────────────────────────────────────────────────────────────────────────
172
+ # Link Types
173
+ # ───────────────────────────────────────────────────────────────────────────
174
+
175
+ self.create_link_type(verb = Safe_Str__Link_Verb('blocks') ,
176
+ inverse_verb = 'blocked-by' ,
177
+ description = 'Prevents progress on target' ,
178
+ source_types = ['bug', 'task'] ,
179
+ target_types = ['task', 'feature'] )
180
+
181
+ self.create_link_type(verb = Safe_Str__Link_Verb('has-task') ,
182
+ inverse_verb = 'task-of' ,
183
+ description = 'Contains as sub-work' ,
184
+ source_types = ['feature', 'git-repo'] , # git-repo can have tasks
185
+ target_types = ['task'] )
186
+
187
+ self.create_link_type(verb = Safe_Str__Link_Verb('assigned-to') ,
188
+ inverse_verb = 'assignee-of' ,
189
+ description = 'Work assigned to person/agent' ,
190
+ source_types = ['bug', 'task', 'feature'] ,
191
+ target_types = ['person'] )
192
+
193
+ self.create_link_type(verb = Safe_Str__Link_Verb('depends-on') ,
194
+ inverse_verb = 'dependency-of' ,
195
+ description = 'Requires target to complete first' ,
196
+ source_types = ['task', 'feature'] ,
197
+ target_types = ['task', 'feature'] )
198
+
199
+ self.create_link_type(verb = Safe_Str__Link_Verb('relates-to') ,
200
+ inverse_verb = 'relates-to' ,
201
+ description = 'General association (symmetric)' ,
202
+ source_types = ['bug', 'task', 'feature', 'git-repo'] ,
203
+ target_types = ['bug', 'task', 'feature', 'git-repo'] )
204
+
205
+ self.create_link_type(verb = Safe_Str__Link_Verb('contains') , # NEW: For hierarchical structure
206
+ inverse_verb = 'contained-by' ,
207
+ description = 'Parent contains child issue' ,
208
+ source_types = ['git-repo', 'feature', 'task'] ,
209
+ target_types = ['bug', 'task', 'feature'] )
@@ -0,0 +1,3 @@
1
+ # ═══════════════════════════════════════════════════════════════════════════════
2
+ # Issue tracking services package
3
+ # ═══════════════════════════════════════════════════════════════════════════════