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,340 @@
1
+ # ═══════════════════════════════════════════════════════════════════════════════
2
+ # Issue__Children__Service - Service for managing child issues in issues/ folders
3
+ # Phase 1: Enables adding children to issues and converting to hierarchical structure
4
+ #
5
+ # Key Operations:
6
+ # - add_child_issue: Create a child issue in parent's issues/ folder
7
+ # - convert_to_new_structure: Create issues/ folder for an existing issue
8
+ # - list_children: List all children in an issue's issues/ folder
9
+ # ═══════════════════════════════════════════════════════════════════════════════
10
+
11
+ from typing import List
12
+ from osbot_utils.type_safe.Type_Safe import Type_Safe
13
+ from osbot_utils.type_safe.primitives.core.Safe_UInt import Safe_UInt
14
+ from osbot_utils.type_safe.primitives.domains.files.safe_str.Safe_Str__File__Path import Safe_Str__File__Path
15
+ from osbot_utils.type_safe.primitives.domains.identifiers.Node_Id import Node_Id
16
+ from osbot_utils.type_safe.primitives.domains.identifiers.Obj_Id import Obj_Id
17
+ from osbot_utils.type_safe.primitives.domains.identifiers.safe_int.Timestamp_Now import Timestamp_Now
18
+ from osbot_utils.type_safe.type_safe_core.decorators.type_safe import type_safe
19
+ from osbot_utils.utils.Json import json_dumps, json_loads
20
+ from issues_fs.schemas.graph.Safe_Str__Graph_Types import Safe_Str__Node_Type, Safe_Str__Node_Label, Safe_Str__Status
21
+ from issues_fs.schemas.graph.Schema__Node import Schema__Node
22
+ from issues_fs.schemas.issues.phase_1.Schema__Issue__Children import Schema__Issue__Child__Create, Schema__Issue__Child__Response, Schema__Issue__Convert__Response, Schema__Issue__Children__List__Response
23
+ from issues_fs.issues.graph_services.Graph__Repository import Graph__Repository
24
+ from issues_fs.issues.storage.Path__Handler__Graph_Node import Path__Handler__Graph_Node, FILE_NAME__ISSUE_JSON
25
+
26
+
27
+ # todo: fix casting to types (like str(..) ) , which is not needed since Type_Safe handles that well (as long are we go into a Type_Safe class, primitive of decorator)
28
+ # create vulns for path transversal issues (i.e. all places where are are doing a path combine using strings
29
+ # see how we can make this work with the Safe_Str__File__Path , for example using the same trick with "/" that Path uses
30
+ # we could use this to create a variation of Safe_Str__File__Path that did not supported path transversal attacks
31
+
32
+ class Issue__Children__Service(Type_Safe): # Service for child issue management
33
+ repository : Graph__Repository # Data access layer
34
+ path_handler : Path__Handler__Graph_Node # Path generation
35
+
36
+ # ═══════════════════════════════════════════════════════════════════════════════
37
+ # Add Child Issue
38
+ # ═══════════════════════════════════════════════════════════════════════════════
39
+
40
+ @type_safe
41
+ def add_child_issue(self , # Add child issue to parent
42
+ parent_path : Safe_Str__File__Path , # Path to parent (relative or full)
43
+ child_data : Schema__Issue__Child__Create
44
+ ) -> Schema__Issue__Child__Response:
45
+ full_parent_path = self.resolve_full_path(str(parent_path)) # Resolve parent path
46
+
47
+ if self.parent_exists(full_parent_path) is False: # Validate parent exists
48
+ return Schema__Issue__Child__Response(success = False ,
49
+ message = f'Parent not found: {parent_path}' )
50
+
51
+ issues_folder = f"{full_parent_path}/issues" # Ensure issues/ folder exists
52
+ self.ensure_folder_exists(issues_folder)
53
+
54
+ child_type = str(child_data.issue_type) # Generate label for child
55
+ child_label = self.generate_child_label(issues_folder, child_type)
56
+
57
+ child_folder = f"{issues_folder}/{child_label}" # Create child folder
58
+ self.ensure_folder_exists(child_folder)
59
+
60
+ now = Timestamp_Now() # Create child issue
61
+ child_issue = Schema__Node(node_id = Node_Id() ,
62
+ node_type = Safe_Str__Node_Type(child_type) ,
63
+ node_index = self.extract_index_from_label(child_label) ,
64
+ label = Safe_Str__Node_Label(child_label) ,
65
+ title = str(child_data.title) ,
66
+ description = str(child_data.description) if child_data.description else '',
67
+ status = Safe_Str__Status(str(child_data.status)) if child_data.status else Safe_Str__Status('backlog'),
68
+ created_at = now ,
69
+ updated_at = now ,
70
+ created_by = Obj_Id() ,
71
+ tags = [] ,
72
+ links = [] ,
73
+ properties = {} )
74
+
75
+ child_path = f"{child_folder}/{FILE_NAME__ISSUE_JSON}" # Save child issue
76
+ data = child_issue.json()
77
+ content = json_dumps(data, indent=2)
78
+ saved = self.repository.storage_fs.file__save(child_path, content.encode('utf-8'))
79
+
80
+ if saved is False:
81
+ return Schema__Issue__Child__Response(success = False ,
82
+ message = 'Failed to save child issue' )
83
+
84
+ relative_child_path = self.make_relative_path(child_folder) # Calculate relative path for response
85
+
86
+ return Schema__Issue__Child__Response(success = True ,
87
+ path = relative_child_path ,
88
+ label = child_label ,
89
+ issue_type = child_type ,
90
+ title = str(child_data.title))
91
+
92
+ # ═══════════════════════════════════════════════════════════════════════════════
93
+ # Convert to New Structure
94
+ # ═══════════════════════════════════════════════════════════════════════════════
95
+
96
+ @type_safe
97
+ def convert_to_new_structure(self , # Create issues/ folder for issue
98
+ issue_path : Safe_Str__File__Path
99
+ ) -> Schema__Issue__Convert__Response:
100
+ full_path = self.resolve_full_path(str(issue_path))
101
+
102
+ if self.parent_exists(full_path) is False: # Validate issue exists
103
+ return Schema__Issue__Convert__Response(success = False ,
104
+ message = f'Issue not found: {issue_path}' )
105
+
106
+ issues_folder = f"{full_path}/issues"
107
+
108
+ if self.folder_exists(issues_folder): # Check if already converted
109
+ return Schema__Issue__Convert__Response(success = True ,
110
+ converted = False ,
111
+ issues_path = self.make_relative_path(issues_folder),
112
+ message = 'Already has issues/ folder' )
113
+
114
+ self.ensure_folder_exists(issues_folder) # Create the issues/ folder
115
+
116
+ placeholder_path = f"{issues_folder}/.gitkeep" # Create placeholder to ensure folder persists
117
+ self.repository.storage_fs.file__save(placeholder_path, b'')
118
+
119
+ return Schema__Issue__Convert__Response(success = True ,
120
+ converted = True ,
121
+ issues_path = self.make_relative_path(issues_folder) ,
122
+ message = 'Created issues/ folder' )
123
+
124
+ # ═══════════════════════════════════════════════════════════════════════════════
125
+ # List Children
126
+ # ═══════════════════════════════════════════════════════════════════════════════
127
+
128
+ @type_safe
129
+ def list_children(self , # List children in issues/ folder
130
+ parent_path : Safe_Str__File__Path
131
+ ) -> Schema__Issue__Children__List__Response:
132
+ full_path = self.resolve_full_path(str(parent_path))
133
+ issues_folder = f"{full_path}/issues"
134
+
135
+ if self.folder_exists(issues_folder) is False: # Check if issues/ folder exists
136
+ return Schema__Issue__Children__List__Response(success = True ,
137
+ children = [] ,
138
+ total = Safe_UInt(0))
139
+
140
+ children = []
141
+ child_folders = self.scan_child_folders(issues_folder) # Scan for child issue folders
142
+
143
+ for child_folder in sorted(child_folders):
144
+ child_data = self.load_child_summary(child_folder)
145
+ if child_data:
146
+ children.append(child_data)
147
+
148
+ return Schema__Issue__Children__List__Response(success = True ,
149
+ children = children ,
150
+ total = Safe_UInt(len(children)))
151
+
152
+ # ═══════════════════════════════════════════════════════════════════════════════
153
+ # Path Resolution Helpers
154
+ # ═══════════════════════════════════════════════════════════════════════════════
155
+
156
+ def resolve_full_path(self, path: str) -> str: # Convert relative path to full path
157
+ if not path:
158
+ base_path = str(self.path_handler.base_path)
159
+ if base_path and base_path != '.':
160
+ return base_path
161
+ return '' # Empty base_path = root is ''
162
+
163
+ base_path = str(self.path_handler.base_path)
164
+
165
+ if not base_path or base_path == '.': # No base_path, path is already relative to storage root
166
+ return path
167
+
168
+ if path.startswith(base_path):
169
+ return path
170
+
171
+ return f"{base_path}/{path}"
172
+
173
+ def make_relative_path(self, full_path: str) -> str: # Convert full path to relative
174
+ base_path = str(self.path_handler.base_path)
175
+
176
+ if not base_path or base_path == '.': # No base_path, path is already relative
177
+ return full_path
178
+
179
+ prefix = f"{base_path}/"
180
+ if full_path.startswith(prefix):
181
+ return full_path[len(prefix):]
182
+
183
+ return full_path
184
+
185
+ # ═══════════════════════════════════════════════════════════════════════════════
186
+ # Folder Operations
187
+ # ═══════════════════════════════════════════════════════════════════════════════
188
+
189
+ def parent_exists(self, folder_path: str) -> bool: # Check if parent issue exists
190
+ issue_json = f"{folder_path}/{FILE_NAME__ISSUE_JSON}" if folder_path else FILE_NAME__ISSUE_JSON
191
+ node_json = f"{folder_path}/node.json" if folder_path else "node.json"
192
+
193
+ if self.repository.storage_fs.file__exists(issue_json):
194
+ return True
195
+ if self.repository.storage_fs.file__exists(node_json):
196
+ return True
197
+
198
+ base_path = str(self.path_handler.base_path) # Root is always valid parent
199
+ if not folder_path or folder_path == base_path or folder_path == '.' or folder_path == '':
200
+ return True
201
+
202
+ return False
203
+
204
+ def folder_exists(self, folder_path: str) -> bool: # Check if folder exists (has any files)
205
+ all_paths = self.repository.storage_fs.files__paths()
206
+ prefix = f"{folder_path}/"
207
+
208
+ for path in all_paths:
209
+ if path.startswith(prefix):
210
+ return True
211
+
212
+ return False
213
+
214
+ def ensure_folder_exists(self, folder_path: str) -> None: # Ensure folder exists in storage
215
+ # todo: see if we need this method
216
+ # if we do add the logic to create the folder when not in memory
217
+ # if we don't need it delete this method
218
+ pass # Memory-FS creates folders implicitly
219
+
220
+ @type_safe
221
+ def scan_child_folders(self, # Find all child folders in issues/
222
+ issues_folder: Safe_Str__File__Path
223
+ ) -> List[Safe_Str__File__Path]:
224
+ folders = set()
225
+ all_paths = self.repository.storage_fs.files__paths()
226
+ prefix = f"{issues_folder}/"
227
+
228
+ for path in all_paths:
229
+ if path.startswith(prefix) is False:
230
+ continue
231
+
232
+ relative = path[len(prefix):] # Remove prefix
233
+ parts = relative.split('/')
234
+
235
+ if len(parts) >= 2:
236
+ child_folder = parts[0] # First segment is child folder
237
+ filename = parts[1]
238
+
239
+ if filename in (FILE_NAME__ISSUE_JSON, 'node.json'):
240
+ folders.add(f"{issues_folder}/{child_folder}")
241
+
242
+ return folders
243
+
244
+ # ═══════════════════════════════════════════════════════════════════════════════
245
+ # Label Generation
246
+ # ═══════════════════════════════════════════════════════════════════════════════
247
+
248
+ def generate_child_label(self , # Generate label for new child
249
+ issues_folder : Safe_Str__File__Path ,
250
+ child_type : str
251
+ ) -> str:
252
+ existing_indices = self.get_existing_indices(issues_folder, child_type) # Find highest existing index
253
+
254
+ next_index = 1
255
+ if existing_indices:
256
+ next_index = max(existing_indices) + 1
257
+
258
+ display_type = child_type.capitalize() # Generate label: "Task" + "-" + "1"
259
+
260
+ if '-' in child_type: # Handle types like "git-repo"
261
+ parts = child_type.split('-')
262
+ display_type = ''.join(p.capitalize() for p in parts)
263
+
264
+ return f"{display_type}-{next_index}"
265
+
266
+ # todo: these str should be type_safe primitives
267
+ def get_existing_indices(self , # Get all existing indices for type
268
+ issues_folder : Safe_Str__File__Path ,
269
+ child_type : str
270
+ ) -> List[int]:
271
+ indices = []
272
+ all_paths = self.repository.storage_fs.files__paths()
273
+ prefix = f"{issues_folder}/"
274
+
275
+ display_type = child_type.capitalize()
276
+ if '-' in child_type:
277
+ parts = child_type.split('-')
278
+ display_type = ''.join(p.capitalize() for p in parts)
279
+
280
+ label_prefix = f"{display_type}-"
281
+
282
+ for path in all_paths:
283
+ if path.startswith(prefix) is False:
284
+ continue
285
+
286
+ relative = path[len(prefix):]
287
+ parts = relative.split('/')
288
+
289
+ if len(parts) >= 1:
290
+ folder_name = parts[0]
291
+
292
+ if folder_name.startswith(label_prefix):
293
+ try:
294
+ index_str = folder_name[len(label_prefix):]
295
+ index = int(index_str)
296
+ indices.append(index)
297
+ except ValueError:
298
+ pass
299
+
300
+ return indices
301
+
302
+ def extract_index_from_label(self, label: str) -> Safe_UInt: # Extract index number from label
303
+ if '-' in label:
304
+ try:
305
+ index_str = label.rsplit('-', 1)[1]
306
+ return Safe_UInt(int(index_str))
307
+ except (ValueError, IndexError):
308
+ pass
309
+ return Safe_UInt(1)
310
+
311
+ # ═══════════════════════════════════════════════════════════════════════════════
312
+ # Child Data Loading
313
+ # ═══════════════════════════════════════════════════════════════════════════════
314
+
315
+ # todo: this should not be a raw dict
316
+ def load_child_summary(self, child_folder: Safe_Str__File__Path) -> dict: # Load summary data for child
317
+ issue_path = f"{child_folder}/{FILE_NAME__ISSUE_JSON}" # Try issue.json first
318
+ data = self.load_issue_from_path(issue_path)
319
+ if data:
320
+ data['path'] = self.make_relative_path(child_folder)
321
+ return data
322
+
323
+ node_path = f"{child_folder}/node.json" # Fall back to node.json
324
+ data = self.load_issue_from_path(node_path)
325
+ if data:
326
+ data['path'] = self.make_relative_path(child_folder)
327
+ return data
328
+
329
+ return None
330
+
331
+ # todo: this should not be a raw dict
332
+ def load_issue_from_path(self, file_path: Safe_Str__File__Path) -> dict: # Load issue JSON from path
333
+ if self.repository.storage_fs.file__exists(file_path) is False:
334
+ return None
335
+
336
+ content = self.repository.storage_fs.file__str(file_path)
337
+ if not content:
338
+ return None
339
+
340
+ return json_loads(content)
@@ -0,0 +1,137 @@
1
+ # ═══════════════════════════════════════════════════════════════════════════════
2
+ # Root__Issue__Service - Service for creating and managing the root issue
3
+ # Phase 1: Creates GitRepo-1 issue in .issues/issue.json
4
+ # ═══════════════════════════════════════════════════════════════════════════════
5
+
6
+ from osbot_utils.type_safe.Type_Safe import Type_Safe
7
+ from osbot_utils.type_safe.primitives.core.Safe_UInt import Safe_UInt
8
+ from osbot_utils.type_safe.primitives.domains.common.safe_str.Safe_Str__Text import Safe_Str__Text
9
+ from osbot_utils.type_safe.primitives.domains.identifiers.Node_Id import Node_Id
10
+ from osbot_utils.type_safe.primitives.domains.identifiers.Obj_Id import Obj_Id
11
+ from osbot_utils.type_safe.primitives.domains.identifiers.safe_int.Timestamp_Now import Timestamp_Now
12
+ from osbot_utils.type_safe.type_safe_core.decorators.type_safe import type_safe
13
+ from osbot_utils.utils.Json import json_dumps, json_loads
14
+ from issues_fs.schemas.graph.Safe_Str__Graph_Types import Safe_Str__Node_Type, Safe_Str__Node_Label, Safe_Str__Status
15
+ from issues_fs.schemas.graph.Schema__Node import Schema__Node
16
+ from issues_fs.issues.graph_services.Graph__Repository import Graph__Repository
17
+ from issues_fs.issues.storage.Path__Handler__Graph_Node import Path__Handler__Graph_Node
18
+
19
+
20
+ # ═══════════════════════════════════════════════════════════════════════════════
21
+ # Constants for Root Issue
22
+ # ═══════════════════════════════════════════════════════════════════════════════
23
+
24
+ ROOT_ISSUE_TYPE = 'git-repo'
25
+ ROOT_ISSUE_LABEL = 'Gitrepo-1'
26
+ ROOT_ISSUE_DEFAULT_TITLE = 'Project Issues'
27
+ ROOT_ISSUE_STATUS = 'active'
28
+
29
+
30
+ class Root__Issue__Service(Type_Safe): # Service for root issue management
31
+ repository : Graph__Repository # Data access layer
32
+ path_handler : Path__Handler__Graph_Node # Path generation
33
+
34
+ # ═══════════════════════════════════════════════════════════════════════════════
35
+ # Root Issue Initialization
36
+ # ═══════════════════════════════════════════════════════════════════════════════
37
+
38
+ def ensure_root_issue_exists(self , # Create root issue if not exists
39
+ title : str = ROOT_ISSUE_DEFAULT_TITLE
40
+ ) -> bool:
41
+ if self.root_issue_exists():
42
+ return True # Already exists
43
+
44
+ return self.create_root_issue(title=title)
45
+
46
+ @type_safe
47
+ def create_root_issue(self , # Create the root GitRepo-1 issue
48
+ title : str = ROOT_ISSUE_DEFAULT_TITLE
49
+ ) -> bool:
50
+ if self.root_issue_exists(): # Don't overwrite existing
51
+ return False
52
+
53
+ now = Timestamp_Now()
54
+
55
+ root_issue = Schema__Node(node_id = Node_Id(Obj_Id()) ,
56
+ node_type = Safe_Str__Node_Type(ROOT_ISSUE_TYPE) ,
57
+ node_index = Safe_UInt(1) ,
58
+ label = Safe_Str__Node_Label(ROOT_ISSUE_LABEL) ,
59
+ title = title ,
60
+ description = 'Root issue for all project issues' ,
61
+ status = Safe_Str__Status(ROOT_ISSUE_STATUS) ,
62
+ created_at = now ,
63
+ updated_at = now ,
64
+ created_by = Obj_Id() ,
65
+ tags = [] ,
66
+ links = [] ,
67
+ properties = {} )
68
+
69
+ return self.save_root_issue(root_issue)
70
+
71
+ # ═══════════════════════════════════════════════════════════════════════════════
72
+ # Root Issue Persistence
73
+ # ═══════════════════════════════════════════════════════════════════════════════
74
+
75
+ def save_root_issue(self, root_issue: Schema__Node) -> bool: # Save root issue to .issues/issue.json
76
+ path = self.path_handler.path_for_root_issue()
77
+ data = root_issue.json()
78
+ content = json_dumps(data, indent=2)
79
+ return self.repository.storage_fs.file__save(path, content.encode('utf-8'))
80
+
81
+ def root_issue_exists(self) -> bool: # Check if root issue.json exists
82
+ path = self.path_handler.path_for_root_issue()
83
+ return self.repository.storage_fs.file__exists(path)
84
+
85
+ def load_root_issue(self) -> Schema__Node: # Load the root issue
86
+ path = self.path_handler.path_for_root_issue()
87
+
88
+ if self.repository.storage_fs.file__exists(path) is False:
89
+ return None
90
+
91
+ content = self.repository.storage_fs.file__str(path)
92
+ if not content:
93
+ return None
94
+
95
+ data = json_loads(content)
96
+ if data is None:
97
+ return None
98
+
99
+ return Schema__Node.from_json(data)
100
+
101
+ # ═══════════════════════════════════════════════════════════════════════════════
102
+ # Root Issue Updates
103
+ # ═══════════════════════════════════════════════════════════════════════════════
104
+
105
+ @type_safe
106
+ def update_root_issue_title(self, title: Safe_Str__Text) -> bool: # Update root issue title
107
+ root_issue = self.load_root_issue()
108
+ if root_issue is None:
109
+ return False
110
+
111
+ root_issue.title = str(title)
112
+ root_issue.updated_at = Timestamp_Now()
113
+
114
+ return self.save_root_issue(root_issue)
115
+
116
+ @type_safe
117
+ def update_root_issue_description(self , # Update root issue description
118
+ description : Safe_Str__Text
119
+ ) -> bool:
120
+ root_issue = self.load_root_issue()
121
+ if root_issue is None:
122
+ return False
123
+
124
+ root_issue.description = str(description)
125
+ root_issue.updated_at = Timestamp_Now()
126
+
127
+ return self.save_root_issue(root_issue)
128
+
129
+ # ═══════════════════════════════════════════════════════════════════════════════
130
+ # Root Issue Deletion (for testing)
131
+ # ═══════════════════════════════════════════════════════════════════════════════
132
+
133
+ def delete_root_issue(self) -> bool: # Delete root issue (for tests)
134
+ path = self.path_handler.path_for_root_issue()
135
+ if self.repository.storage_fs.file__exists(path):
136
+ return self.repository.storage_fs.file__delete(path)
137
+ return False