elspais 0.11.1__py3-none-any.whl → 0.11.2__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 (53) hide show
  1. elspais/__init__.py +1 -1
  2. elspais/cli.py +29 -10
  3. elspais/commands/analyze.py +5 -6
  4. elspais/commands/changed.py +2 -6
  5. elspais/commands/config_cmd.py +4 -4
  6. elspais/commands/edit.py +32 -36
  7. elspais/commands/hash_cmd.py +24 -18
  8. elspais/commands/index.py +8 -7
  9. elspais/commands/init.py +4 -4
  10. elspais/commands/reformat_cmd.py +32 -43
  11. elspais/commands/rules_cmd.py +6 -2
  12. elspais/commands/trace.py +23 -19
  13. elspais/commands/validate.py +8 -10
  14. elspais/config/defaults.py +7 -1
  15. elspais/core/content_rules.py +0 -1
  16. elspais/core/git.py +4 -10
  17. elspais/core/parser.py +55 -56
  18. elspais/core/patterns.py +2 -6
  19. elspais/core/rules.py +10 -15
  20. elspais/mcp/__init__.py +2 -0
  21. elspais/mcp/context.py +1 -0
  22. elspais/mcp/serializers.py +1 -1
  23. elspais/mcp/server.py +54 -39
  24. elspais/reformat/__init__.py +13 -13
  25. elspais/reformat/detector.py +9 -16
  26. elspais/reformat/hierarchy.py +8 -7
  27. elspais/reformat/line_breaks.py +36 -38
  28. elspais/reformat/prompts.py +22 -12
  29. elspais/reformat/transformer.py +43 -41
  30. elspais/sponsors/__init__.py +0 -2
  31. elspais/testing/__init__.py +1 -1
  32. elspais/testing/result_parser.py +25 -21
  33. elspais/trace_view/__init__.py +4 -3
  34. elspais/trace_view/coverage.py +5 -5
  35. elspais/trace_view/generators/__init__.py +1 -1
  36. elspais/trace_view/generators/base.py +17 -12
  37. elspais/trace_view/generators/csv.py +2 -6
  38. elspais/trace_view/generators/markdown.py +3 -8
  39. elspais/trace_view/html/__init__.py +4 -2
  40. elspais/trace_view/html/generator.py +423 -289
  41. elspais/trace_view/models.py +25 -0
  42. elspais/trace_view/review/__init__.py +21 -18
  43. elspais/trace_view/review/branches.py +114 -121
  44. elspais/trace_view/review/models.py +232 -237
  45. elspais/trace_view/review/position.py +53 -71
  46. elspais/trace_view/review/server.py +264 -288
  47. elspais/trace_view/review/status.py +43 -58
  48. elspais/trace_view/review/storage.py +48 -72
  49. {elspais-0.11.1.dist-info → elspais-0.11.2.dist-info}/METADATA +1 -1
  50. {elspais-0.11.1.dist-info → elspais-0.11.2.dist-info}/RECORD +53 -53
  51. {elspais-0.11.1.dist-info → elspais-0.11.2.dist-info}/WHEEL +0 -0
  52. {elspais-0.11.1.dist-info → elspais-0.11.2.dist-info}/entry_points.txt +0 -0
  53. {elspais-0.11.1.dist-info → elspais-0.11.2.dist-info}/licenses/LICENSE +0 -0
@@ -28,22 +28,20 @@ VALID_STATUSES = {"Draft", "Active", "Deprecated"}
28
28
  # Regex pattern to match the status line in a requirement
29
29
  # Matches: **Level**: Dev | **Status**: Draft | **Implements**: REQ-xxx
30
30
  STATUS_LINE_PATTERN = re.compile(
31
- r'^(\*\*Level\*\*:\s+(?:PRD|Ops|Dev)\s+\|\s+'
32
- r'\*\*Status\*\*:\s+)(Draft|Active|Deprecated)'
33
- r'(\s+\|\s+\*\*Implements\*\*:\s+[^\n]*?)$',
34
- re.MULTILINE
31
+ r"^(\*\*Level\*\*:\s+(?:PRD|Ops|Dev)\s+\|\s+"
32
+ r"\*\*Status\*\*:\s+)(Draft|Active|Deprecated)"
33
+ r"(\s+\|\s+\*\*Implements\*\*:\s+[^\n]*?)$",
34
+ re.MULTILINE,
35
35
  )
36
36
 
37
37
  # Pattern to find a REQ header (supports both REQ-tv-xxx and REQ-SPONSOR-xxx formats)
38
38
  REQ_HEADER_PATTERN = re.compile(
39
- r'^#{1,6}\s+REQ-(?:([A-Za-z]{2,4})-)?([pod]\d{5}):\s+(.+)$',
40
- re.MULTILINE
39
+ r"^#{1,6}\s+REQ-(?:([A-Za-z]{2,4})-)?([pod]\d{5}):\s+(.+)$", re.MULTILINE
41
40
  )
42
41
 
43
42
  # Pattern to find the End footer with hash
44
43
  REQ_FOOTER_PATTERN = re.compile(
45
- r'^\*End\* \*([^*]+)\* \| \*\*Hash\*\*: ([a-f0-9]{8})$',
46
- re.MULTILINE
44
+ r"^\*End\* \*([^*]+)\* \| \*\*Hash\*\*: ([a-f0-9]{8})$", re.MULTILINE
47
45
  )
48
46
 
49
47
 
@@ -52,9 +50,11 @@ REQ_FOOTER_PATTERN = re.compile(
52
50
  # REQ-tv-d00015-A: Return structured location information
53
51
  # =============================================================================
54
52
 
53
+
55
54
  @dataclass
56
55
  class ReqLocation:
57
56
  """Location of a requirement in a spec file."""
57
+
58
58
  file_path: Path
59
59
  line_number: int # 1-based line number of status line
60
60
  current_status: str
@@ -66,6 +66,7 @@ class ReqLocation:
66
66
  # REQ-tv-d00015-F: Content hash computation and update
67
67
  # =============================================================================
68
68
 
69
+
69
70
  def compute_req_hash(content: str) -> str:
70
71
  """
71
72
  Compute an 8-character hex hash of requirement content.
@@ -79,7 +80,7 @@ def compute_req_hash(content: str) -> str:
79
80
  Returns:
80
81
  8-character lowercase hex string
81
82
  """
82
- return hashlib.sha256(content.encode('utf-8')).hexdigest()[:8]
83
+ return hashlib.sha256(content.encode("utf-8")).hexdigest()[:8]
83
84
 
84
85
 
85
86
  def _extract_req_content(file_content: str, req_id: str) -> Optional[Tuple[str, int, int]]:
@@ -96,18 +97,16 @@ def _extract_req_content(file_content: str, req_id: str) -> Optional[Tuple[str,
96
97
 
97
98
  # Build pattern for this specific requirement
98
99
  # Handle both "tv-d00010" and "HHT-d00001" formats
99
- if '-' in normalized_id:
100
- parts = normalized_id.split('-', 1)
100
+ if "-" in normalized_id:
101
+ parts = normalized_id.split("-", 1)
101
102
  prefix = parts[0]
102
103
  base_id = parts[1]
103
104
  header_pattern = re.compile(
104
- rf'^#{{1,6}}\s+REQ-{re.escape(prefix)}-{re.escape(base_id)}:\s+(.+)$',
105
- re.MULTILINE
105
+ rf"^#{{1,6}}\s+REQ-{re.escape(prefix)}-{re.escape(base_id)}:\s+(.+)$", re.MULTILINE
106
106
  )
107
107
  else:
108
108
  header_pattern = re.compile(
109
- rf'^#{{1,6}}\s+REQ-{re.escape(normalized_id)}:\s+(.+)$',
110
- re.MULTILINE
109
+ rf"^#{{1,6}}\s+REQ-{re.escape(normalized_id)}:\s+(.+)$", re.MULTILINE
111
110
  )
112
111
 
113
112
  header_match = header_pattern.search(file_content)
@@ -120,8 +119,7 @@ def _extract_req_content(file_content: str, req_id: str) -> Optional[Tuple[str,
120
119
  # Find the footer for this requirement
121
120
  # The footer format is: *End* *{title}* | **Hash**: {hash}
122
121
  footer_pattern = re.compile(
123
- rf'^\*End\* \*{re.escape(req_title)}\* \| \*\*Hash\*\*: ([a-f0-9]{{8}})$',
124
- re.MULTILINE
122
+ rf"^\*End\* \*{re.escape(req_title)}\* \| \*\*Hash\*\*: ([a-f0-9]{{8}})$", re.MULTILINE
125
123
  )
126
124
 
127
125
  # Search from after the header
@@ -155,8 +153,8 @@ def update_req_hash(file_path: Path, req_id: str) -> bool:
155
153
  True if hash was updated, False if requirement not found
156
154
  """
157
155
  try:
158
- content = file_path.read_text(encoding='utf-8')
159
- except (FileNotFoundError, IOError):
156
+ content = file_path.read_text(encoding="utf-8")
157
+ except (OSError, FileNotFoundError):
160
158
  return False
161
159
 
162
160
  result = _extract_req_content(content, req_id)
@@ -185,6 +183,7 @@ def update_req_hash(file_path: Path, req_id: str) -> bool:
185
183
  # REQ-tv-d00015-A: find_req_in_file() SHALL locate a requirement
186
184
  # =============================================================================
187
185
 
186
+
188
187
  def find_req_in_file(file_path: Path, req_id: str) -> Optional[ReqLocation]:
189
188
  """
190
189
  Find a requirement in a spec file and return its position info.
@@ -201,8 +200,8 @@ def find_req_in_file(file_path: Path, req_id: str) -> Optional[ReqLocation]:
201
200
  ReqLocation with req info if found, None otherwise
202
201
  """
203
202
  try:
204
- content = file_path.read_text(encoding='utf-8')
205
- except (FileNotFoundError, IOError):
203
+ content = file_path.read_text(encoding="utf-8")
204
+ except (OSError, FileNotFoundError):
206
205
  return None
207
206
 
208
207
  # Normalize req_id (remove REQ- prefix if present)
@@ -212,18 +211,16 @@ def find_req_in_file(file_path: Path, req_id: str) -> Optional[ReqLocation]:
212
211
 
213
212
  # Build pattern for this specific requirement
214
213
  # Handle both "tv-d00010" and "HHT-d00001" formats
215
- if '-' in normalized_id:
216
- parts = normalized_id.split('-', 1)
214
+ if "-" in normalized_id:
215
+ parts = normalized_id.split("-", 1)
217
216
  prefix = parts[0]
218
217
  base_id = parts[1]
219
218
  header_pattern = re.compile(
220
- rf'^#{{1,6}}\s+REQ-{re.escape(prefix)}-{re.escape(base_id)}:\s+.+$',
221
- re.MULTILINE
219
+ rf"^#{{1,6}}\s+REQ-{re.escape(prefix)}-{re.escape(base_id)}:\s+.+$", re.MULTILINE
222
220
  )
223
221
  else:
224
222
  header_pattern = re.compile(
225
- rf'^#{{1,6}}\s+REQ-{re.escape(normalized_id)}:\s+.+$',
226
- re.MULTILINE
223
+ rf"^#{{1,6}}\s+REQ-{re.escape(normalized_id)}:\s+.+$", re.MULTILINE
227
224
  )
228
225
 
229
226
  header_match = header_pattern.search(content)
@@ -246,13 +243,13 @@ def find_req_in_file(file_path: Path, req_id: str) -> Optional[ReqLocation]:
246
243
  current_status = status_match.group(2)
247
244
 
248
245
  # Calculate 1-based line number
249
- line_number = content[:status_match.start()].count('\n') + 1
246
+ line_number = content[: status_match.start()].count("\n") + 1
250
247
 
251
248
  return ReqLocation(
252
249
  file_path=file_path,
253
250
  line_number=line_number,
254
251
  current_status=current_status,
255
- req_id=normalized_id
252
+ req_id=normalized_id,
256
253
  )
257
254
 
258
255
 
@@ -273,7 +270,7 @@ def find_req_in_spec_dir(repo_root: Path, req_id: str) -> Optional[ReqLocation]:
273
270
  spec_dir = repo_root / "spec"
274
271
  if spec_dir.exists():
275
272
  for spec_file in spec_dir.glob("*.md"):
276
- if spec_file.name in ('INDEX.md', 'README.md', 'requirements-format.md'):
273
+ if spec_file.name in ("INDEX.md", "README.md", "requirements-format.md"):
277
274
  continue
278
275
  location = find_req_in_file(spec_file, req_id)
279
276
  if location:
@@ -287,7 +284,7 @@ def find_req_in_spec_dir(repo_root: Path, req_id: str) -> Optional[ReqLocation]:
287
284
  sponsor_spec = sponsor / "spec"
288
285
  if sponsor_spec.exists():
289
286
  for spec_file in sponsor_spec.glob("*.md"):
290
- if spec_file.name in ('INDEX.md', 'README.md', 'requirements-format.md'):
287
+ if spec_file.name in ("INDEX.md", "README.md", "requirements-format.md"):
291
288
  continue
292
289
  location = find_req_in_file(spec_file, req_id)
293
290
  if location:
@@ -301,6 +298,7 @@ def find_req_in_spec_dir(repo_root: Path, req_id: str) -> Optional[ReqLocation]:
301
298
  # REQ-tv-d00015-B: get_req_status() SHALL read and return current status
302
299
  # =============================================================================
303
300
 
301
+
304
302
  def get_req_status(repo_root: Path, req_id: str) -> Optional[str]:
305
303
  """
306
304
  Get the current status of a requirement.
@@ -326,6 +324,7 @@ def get_req_status(repo_root: Path, req_id: str) -> Optional[str]:
326
324
  # REQ-tv-d00015-G: Failed status changes SHALL NOT corrupt the file
327
325
  # =============================================================================
328
326
 
327
+
329
328
  def _atomic_write_file(file_path: Path, content: str) -> None:
330
329
  """
331
330
  Atomically write content to a file.
@@ -341,13 +340,9 @@ def _atomic_write_file(file_path: Path, content: str) -> None:
341
340
  file_path.parent.mkdir(parents=True, exist_ok=True)
342
341
 
343
342
  # Write to temp file in same directory (for atomic rename)
344
- fd, temp_path = tempfile.mkstemp(
345
- suffix='.md',
346
- prefix='.tmp_',
347
- dir=file_path.parent
348
- )
343
+ fd, temp_path = tempfile.mkstemp(suffix=".md", prefix=".tmp_", dir=file_path.parent)
349
344
  try:
350
- with os.fdopen(fd, 'w', encoding='utf-8') as f:
345
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
351
346
  f.write(content)
352
347
  # Atomic rename
353
348
  os.rename(temp_path, file_path)
@@ -363,12 +358,8 @@ def _atomic_write_file(file_path: Path, content: str) -> None:
363
358
  # REQ-tv-d00015-C: change_req_status() SHALL update status atomically
364
359
  # =============================================================================
365
360
 
366
- def change_req_status(
367
- repo_root: Path,
368
- req_id: str,
369
- new_status: str,
370
- user: str
371
- ) -> Tuple[bool, str]:
361
+
362
+ def change_req_status(repo_root: Path, req_id: str, new_status: str, user: str) -> Tuple[bool, str]:
372
363
  """
373
364
  Change the status of a requirement in its spec file.
374
365
 
@@ -396,7 +387,7 @@ def change_req_status(
396
387
  """
397
388
  # Validate new_status (REQ-tv-d00015-D)
398
389
  if new_status not in VALID_STATUSES:
399
- valid_list = ', '.join(sorted(VALID_STATUSES))
390
+ valid_list = ", ".join(sorted(VALID_STATUSES))
400
391
  return (False, f"Invalid status '{new_status}'. Valid statuses: {valid_list}")
401
392
 
402
393
  # Find the requirement
@@ -410,8 +401,8 @@ def change_req_status(
410
401
 
411
402
  # Read the file content
412
403
  try:
413
- content = location.file_path.read_text(encoding='utf-8')
414
- except IOError as e:
404
+ content = location.file_path.read_text(encoding="utf-8")
405
+ except OSError as e:
415
406
  return (False, f"Failed to read spec file: {e}")
416
407
 
417
408
  # Normalize req_id for pattern matching
@@ -420,18 +411,16 @@ def change_req_status(
420
411
  normalized_id = normalized_id[4:]
421
412
 
422
413
  # Build pattern for this specific requirement header
423
- if '-' in normalized_id:
424
- parts = normalized_id.split('-', 1)
414
+ if "-" in normalized_id:
415
+ parts = normalized_id.split("-", 1)
425
416
  prefix = parts[0]
426
417
  base_id = parts[1]
427
418
  header_pattern = re.compile(
428
- rf'^#{{1,6}}\s+REQ-{re.escape(prefix)}-{re.escape(base_id)}:\s+.+$',
429
- re.MULTILINE
419
+ rf"^#{{1,6}}\s+REQ-{re.escape(prefix)}-{re.escape(base_id)}:\s+.+$", re.MULTILINE
430
420
  )
431
421
  else:
432
422
  header_pattern = re.compile(
433
- rf'^#{{1,6}}\s+REQ-{re.escape(normalized_id)}:\s+.+$',
434
- re.MULTILINE
423
+ rf"^#{{1,6}}\s+REQ-{re.escape(normalized_id)}:\s+.+$", re.MULTILINE
435
424
  )
436
425
 
437
426
  header_match = header_pattern.search(content)
@@ -451,16 +440,12 @@ def change_req_status(
451
440
  new_line = status_match.group(1) + new_status + status_match.group(3)
452
441
 
453
442
  # Replace the status line in content
454
- new_content = (
455
- content[:status_match.start()] +
456
- new_line +
457
- content[status_match.end():]
458
- )
443
+ new_content = content[: status_match.start()] + new_line + content[status_match.end() :]
459
444
 
460
445
  # Write atomically (REQ-tv-d00015-G)
461
446
  try:
462
447
  _atomic_write_file(location.file_path, new_content)
463
- except IOError as e:
448
+ except OSError as e:
464
449
  return (False, f"Failed to write spec file: {e}")
465
450
 
466
451
  # Update the hash (REQ-tv-d00015-F)
@@ -22,25 +22,25 @@ from pathlib import Path
22
22
  from typing import Any, Dict, List, Optional
23
23
 
24
24
  from .models import (
25
+ Approval,
26
+ Comment,
27
+ PackagesFile,
25
28
  ReviewConfig,
26
29
  ReviewFlag,
27
- Thread,
28
- Comment,
29
- ThreadsFile,
30
+ ReviewPackage,
30
31
  StatusFile,
31
32
  StatusRequest,
32
- Approval,
33
- ReviewPackage,
34
- PackagesFile,
33
+ Thread,
34
+ ThreadsFile,
35
35
  parse_iso_datetime,
36
36
  )
37
37
 
38
-
39
38
  # =============================================================================
40
39
  # Helper Functions
41
40
  # REQ-tv-d00011-A: Atomic write operations
42
41
  # =============================================================================
43
42
 
43
+
44
44
  def atomic_write_json(path: Path, data: Dict[str, Any]) -> None:
45
45
  """
46
46
  Atomically write JSON data to a file.
@@ -56,13 +56,9 @@ def atomic_write_json(path: Path, data: Dict[str, Any]) -> None:
56
56
  path.parent.mkdir(parents=True, exist_ok=True)
57
57
 
58
58
  # Write to temp file in same directory (for atomic rename)
59
- fd, temp_path = tempfile.mkstemp(
60
- suffix='.json',
61
- prefix='.tmp_',
62
- dir=path.parent
63
- )
59
+ fd, temp_path = tempfile.mkstemp(suffix=".json", prefix=".tmp_", dir=path.parent)
64
60
  try:
65
- with os.fdopen(fd, 'w') as f:
61
+ with os.fdopen(fd, "w") as f:
66
62
  json.dump(data, f, indent=2)
67
63
  # Atomic rename
68
64
  os.rename(temp_path, path)
@@ -87,7 +83,7 @@ def read_json(path: Path) -> Dict[str, Any]:
87
83
  FileNotFoundError: If file doesn't exist
88
84
  json.JSONDecodeError: If file contains invalid JSON
89
85
  """
90
- with open(path, 'r') as f:
86
+ with open(path) as f:
91
87
  return json.load(f)
92
88
 
93
89
 
@@ -97,6 +93,7 @@ def read_json(path: Path) -> Dict[str, Any]:
97
93
  # REQ-tv-d00011-I: Requirement ID normalization
98
94
  # =============================================================================
99
95
 
96
+
100
97
  def normalize_req_id(req_id: str) -> str:
101
98
  """
102
99
  Normalize requirement ID for use in file paths.
@@ -109,7 +106,7 @@ def normalize_req_id(req_id: str) -> str:
109
106
  Returns:
110
107
  Normalized requirement ID safe for file paths
111
108
  """
112
- return re.sub(r'[:/]', '_', req_id)
109
+ return re.sub(r"[:/]", "_", req_id)
113
110
 
114
111
 
115
112
  def get_reviews_root(repo_root: Path) -> Path:
@@ -124,7 +121,7 @@ def get_reviews_root(repo_root: Path) -> Path:
124
121
  Returns:
125
122
  Path to .reviews directory
126
123
  """
127
- return repo_root / '.reviews'
124
+ return repo_root / ".reviews"
128
125
 
129
126
 
130
127
  def get_req_dir(repo_root: Path, req_id: str) -> Path:
@@ -141,7 +138,7 @@ def get_req_dir(repo_root: Path, req_id: str) -> Path:
141
138
  Path to requirement's review directory
142
139
  """
143
140
  normalized = normalize_req_id(req_id)
144
- return get_reviews_root(repo_root) / 'reqs' / normalized
141
+ return get_reviews_root(repo_root) / "reqs" / normalized
145
142
 
146
143
 
147
144
  def get_threads_path(repo_root: Path, req_id: str) -> Path:
@@ -157,7 +154,7 @@ def get_threads_path(repo_root: Path, req_id: str) -> Path:
157
154
  Returns:
158
155
  Path to threads.json
159
156
  """
160
- return get_req_dir(repo_root, req_id) / 'threads.json'
157
+ return get_req_dir(repo_root, req_id) / "threads.json"
161
158
 
162
159
 
163
160
  def get_status_path(repo_root: Path, req_id: str) -> Path:
@@ -173,7 +170,7 @@ def get_status_path(repo_root: Path, req_id: str) -> Path:
173
170
  Returns:
174
171
  Path to status.json
175
172
  """
176
- return get_req_dir(repo_root, req_id) / 'status.json'
173
+ return get_req_dir(repo_root, req_id) / "status.json"
177
174
 
178
175
 
179
176
  def get_review_flag_path(repo_root: Path, req_id: str) -> Path:
@@ -189,7 +186,7 @@ def get_review_flag_path(repo_root: Path, req_id: str) -> Path:
189
186
  Returns:
190
187
  Path to flag.json
191
188
  """
192
- return get_req_dir(repo_root, req_id) / 'flag.json'
189
+ return get_req_dir(repo_root, req_id) / "flag.json"
193
190
 
194
191
 
195
192
  def get_config_path(repo_root: Path) -> Path:
@@ -204,7 +201,7 @@ def get_config_path(repo_root: Path) -> Path:
204
201
  Returns:
205
202
  Path to config.json
206
203
  """
207
- return get_reviews_root(repo_root) / 'config.json'
204
+ return get_reviews_root(repo_root) / "config.json"
208
205
 
209
206
 
210
207
  def get_packages_path(repo_root: Path) -> Path:
@@ -219,7 +216,7 @@ def get_packages_path(repo_root: Path) -> Path:
219
216
  Returns:
220
217
  Path to packages.json
221
218
  """
222
- return get_reviews_root(repo_root) / 'packages.json'
219
+ return get_reviews_root(repo_root) / "packages.json"
223
220
 
224
221
 
225
222
  # =============================================================================
@@ -227,6 +224,7 @@ def get_packages_path(repo_root: Path) -> Path:
227
224
  # REQ-d00096: Review Storage Architecture
228
225
  # =============================================================================
229
226
 
227
+
230
228
  def get_index_path(repo_root: Path) -> Path:
231
229
  """
232
230
  Get path to index.json file (v2 format).
@@ -239,7 +237,7 @@ def get_index_path(repo_root: Path) -> Path:
239
237
  Returns:
240
238
  Path to index.json
241
239
  """
242
- return get_reviews_root(repo_root) / 'index.json'
240
+ return get_reviews_root(repo_root) / "index.json"
243
241
 
244
242
 
245
243
  def get_package_dir(repo_root: Path, package_id: str) -> Path:
@@ -255,7 +253,7 @@ def get_package_dir(repo_root: Path, package_id: str) -> Path:
255
253
  Returns:
256
254
  Path to package directory
257
255
  """
258
- return get_reviews_root(repo_root) / 'packages' / package_id
256
+ return get_reviews_root(repo_root) / "packages" / package_id
259
257
 
260
258
 
261
259
  def get_package_metadata_path(repo_root: Path, package_id: str) -> Path:
@@ -271,7 +269,7 @@ def get_package_metadata_path(repo_root: Path, package_id: str) -> Path:
271
269
  Returns:
272
270
  Path to package.json
273
271
  """
274
- return get_package_dir(repo_root, package_id) / 'package.json'
272
+ return get_package_dir(repo_root, package_id) / "package.json"
275
273
 
276
274
 
277
275
  def get_package_threads_path(repo_root: Path, package_id: str, req_id: str) -> Path:
@@ -289,7 +287,7 @@ def get_package_threads_path(repo_root: Path, package_id: str, req_id: str) -> P
289
287
  Path to threads.json
290
288
  """
291
289
  normalized = normalize_req_id(req_id)
292
- return get_package_dir(repo_root, package_id) / 'reqs' / normalized / 'threads.json'
290
+ return get_package_dir(repo_root, package_id) / "reqs" / normalized / "threads.json"
293
291
 
294
292
 
295
293
  def get_archive_dir(repo_root: Path) -> Path:
@@ -304,7 +302,7 @@ def get_archive_dir(repo_root: Path) -> Path:
304
302
  Returns:
305
303
  Path to archive directory
306
304
  """
307
- return get_reviews_root(repo_root) / 'archive'
305
+ return get_reviews_root(repo_root) / "archive"
308
306
 
309
307
 
310
308
  def get_archived_package_dir(repo_root: Path, package_id: str) -> Path:
@@ -336,14 +334,10 @@ def get_archived_package_metadata_path(repo_root: Path, package_id: str) -> Path
336
334
  Returns:
337
335
  Path to archived package.json
338
336
  """
339
- return get_archived_package_dir(repo_root, package_id) / 'package.json'
337
+ return get_archived_package_dir(repo_root, package_id) / "package.json"
340
338
 
341
339
 
342
- def get_archived_package_threads_path(
343
- repo_root: Path,
344
- package_id: str,
345
- req_id: str
346
- ) -> Path:
340
+ def get_archived_package_threads_path(repo_root: Path, package_id: str, req_id: str) -> Path:
347
341
  """
348
342
  Get path to threads.json file for a requirement within an archived package.
349
343
 
@@ -358,7 +352,7 @@ def get_archived_package_threads_path(
358
352
  Path to archived threads.json
359
353
  """
360
354
  normalized = normalize_req_id(req_id)
361
- return get_archived_package_dir(repo_root, package_id) / 'reqs' / normalized / 'threads.json'
355
+ return get_archived_package_dir(repo_root, package_id) / "reqs" / normalized / "threads.json"
362
356
 
363
357
 
364
358
  # =============================================================================
@@ -366,6 +360,7 @@ def get_archived_package_threads_path(
366
360
  # REQ-tv-d00011-F: Config storage operations
367
361
  # =============================================================================
368
362
 
363
+
369
364
  def load_config(repo_root: Path) -> ReviewConfig:
370
365
  """
371
366
  Load review system configuration.
@@ -404,6 +399,7 @@ def save_config(repo_root: Path, config: ReviewConfig) -> None:
404
399
  # REQ-tv-d00011-D: Review flag storage operations
405
400
  # =============================================================================
406
401
 
402
+
407
403
  def load_review_flag(repo_root: Path, req_id: str) -> ReviewFlag:
408
404
  """
409
405
  Load review flag for a requirement.
@@ -444,6 +440,7 @@ def save_review_flag(repo_root: Path, req_id: str, flag: ReviewFlag) -> None:
444
440
  # REQ-tv-d00011-B: Thread storage operations
445
441
  # =============================================================================
446
442
 
443
+
447
444
  def load_threads(repo_root: Path, req_id: str) -> ThreadsFile:
448
445
  """
449
446
  Load threads for a requirement.
@@ -501,11 +498,7 @@ def add_thread(repo_root: Path, req_id: str, thread: Thread) -> Thread:
501
498
 
502
499
 
503
500
  def add_comment_to_thread(
504
- repo_root: Path,
505
- req_id: str,
506
- thread_id: str,
507
- author: str,
508
- body: str
501
+ repo_root: Path, req_id: str, thread_id: str, author: str, body: str
509
502
  ) -> Comment:
510
503
  """
511
504
  Add a comment to an existing thread.
@@ -542,12 +535,7 @@ def add_comment_to_thread(
542
535
  return comment
543
536
 
544
537
 
545
- def resolve_thread(
546
- repo_root: Path,
547
- req_id: str,
548
- thread_id: str,
549
- user: str
550
- ) -> bool:
538
+ def resolve_thread(repo_root: Path, req_id: str, thread_id: str, user: str) -> bool:
551
539
  """
552
540
  Mark a thread as resolved.
553
541
 
@@ -603,6 +591,7 @@ def unresolve_thread(repo_root: Path, req_id: str, thread_id: str) -> bool:
603
591
  # REQ-tv-d00011-C: Status request storage operations
604
592
  # =============================================================================
605
593
 
594
+
606
595
  def load_status_requests(repo_root: Path, req_id: str) -> StatusFile:
607
596
  """
608
597
  Load status requests for a requirement.
@@ -639,11 +628,7 @@ def save_status_requests(repo_root: Path, req_id: str, status_file: StatusFile)
639
628
  atomic_write_json(status_path, status_file.to_dict())
640
629
 
641
630
 
642
- def create_status_request(
643
- repo_root: Path,
644
- req_id: str,
645
- request: StatusRequest
646
- ) -> StatusRequest:
631
+ def create_status_request(repo_root: Path, req_id: str, request: StatusRequest) -> StatusRequest:
647
632
  """
648
633
  Create a new status change request.
649
634
 
@@ -669,7 +654,7 @@ def add_approval(
669
654
  request_id: str,
670
655
  user: str,
671
656
  decision: str,
672
- comment: Optional[str] = None
657
+ comment: Optional[str] = None,
673
658
  ) -> Approval:
674
659
  """
675
660
  Add an approval to a status request.
@@ -740,6 +725,7 @@ def mark_request_applied(repo_root: Path, req_id: str, request_id: str) -> bool:
740
725
  # REQ-tv-d00011-E: Package storage operations
741
726
  # =============================================================================
742
727
 
728
+
743
729
  def load_packages(repo_root: Path) -> PackagesFile:
744
730
  """
745
731
  Load packages file.
@@ -907,6 +893,7 @@ def remove_req_from_package(repo_root: Path, package_id: str, req_id: str) -> bo
907
893
  # REQ-tv-d00011-J: Deduplication and timestamp-based conflict resolution
908
894
  # =============================================================================
909
895
 
896
+
910
897
  def merge_threads(local: ThreadsFile, remote: ThreadsFile) -> ThreadsFile:
911
898
  """
912
899
  Merge thread files from local and remote.
@@ -985,7 +972,7 @@ def _merge_single_thread(local: Thread, remote: Thread) -> Thread:
985
972
  resolved=resolved,
986
973
  resolvedBy=resolved_by,
987
974
  resolvedAt=resolved_at,
988
- comments=merged_comments
975
+ comments=merged_comments,
989
976
  )
990
977
 
991
978
 
@@ -1073,7 +1060,7 @@ def _merge_single_request(local: StatusRequest, remote: StatusRequest) -> Status
1073
1060
  justification=local.justification,
1074
1061
  approvals=merged_approvals,
1075
1062
  requiredApprovers=local.requiredApprovers,
1076
- state=local.state # Will be recalculated
1063
+ state=local.state, # Will be recalculated
1077
1064
  )
1078
1065
 
1079
1066
  # Recalculate state based on merged approvals
@@ -1125,7 +1112,7 @@ def merge_review_flags(local: ReviewFlag, remote: ReviewFlag) -> ReviewFlag:
1125
1112
  flaggedBy=remote.flaggedBy,
1126
1113
  flaggedAt=remote.flaggedAt,
1127
1114
  reason=remote.reason,
1128
- scope=merged_scope
1115
+ scope=merged_scope,
1129
1116
  )
1130
1117
  else:
1131
1118
  # Local is newer
@@ -1134,7 +1121,7 @@ def merge_review_flags(local: ReviewFlag, remote: ReviewFlag) -> ReviewFlag:
1134
1121
  flaggedBy=local.flaggedBy,
1135
1122
  flaggedAt=local.flaggedAt,
1136
1123
  reason=local.reason,
1137
- scope=merged_scope
1124
+ scope=merged_scope,
1138
1125
  )
1139
1126
 
1140
1127
 
@@ -1143,12 +1130,8 @@ def merge_review_flags(local: ReviewFlag, remote: ReviewFlag) -> ReviewFlag:
1143
1130
  # REQ-d00097: Review Package Archival
1144
1131
  # =============================================================================
1145
1132
 
1146
- def archive_package(
1147
- repo_root: Path,
1148
- package_id: str,
1149
- reason: str,
1150
- user: str
1151
- ) -> bool:
1133
+
1134
+ def archive_package(repo_root: Path, package_id: str, reason: str, user: str) -> bool:
1152
1135
  """
1153
1136
  Archive a package by moving it to the archive directory.
1154
1137
 
@@ -1169,9 +1152,9 @@ def archive_package(
1169
1152
  ValueError: If reason is not valid
1170
1153
  """
1171
1154
  from .models import (
1172
- ARCHIVE_REASON_RESOLVED,
1173
1155
  ARCHIVE_REASON_DELETED,
1174
1156
  ARCHIVE_REASON_MANUAL,
1157
+ ARCHIVE_REASON_RESOLVED,
1175
1158
  )
1176
1159
 
1177
1160
  valid_reasons = {ARCHIVE_REASON_RESOLVED, ARCHIVE_REASON_DELETED, ARCHIVE_REASON_MANUAL}
@@ -1239,7 +1222,7 @@ def list_archived_packages(repo_root: Path) -> List[ReviewPackage]:
1239
1222
  packages = []
1240
1223
  for pkg_dir in archive_root.iterdir():
1241
1224
  if pkg_dir.is_dir():
1242
- metadata_path = pkg_dir / 'package.json'
1225
+ metadata_path = pkg_dir / "package.json"
1243
1226
  if metadata_path.exists():
1244
1227
  try:
1245
1228
  data = read_json(metadata_path)
@@ -1250,10 +1233,7 @@ def list_archived_packages(repo_root: Path) -> List[ReviewPackage]:
1250
1233
  continue
1251
1234
 
1252
1235
  # Sort by archive date (most recent first)
1253
- packages.sort(
1254
- key=lambda p: p.archivedAt or '',
1255
- reverse=True
1256
- )
1236
+ packages.sort(key=lambda p: p.archivedAt or "", reverse=True)
1257
1237
 
1258
1238
  return packages
1259
1239
 
@@ -1282,11 +1262,7 @@ def get_archived_package(repo_root: Path, package_id: str) -> Optional[ReviewPac
1282
1262
  return None
1283
1263
 
1284
1264
 
1285
- def load_archived_threads(
1286
- repo_root: Path,
1287
- package_id: str,
1288
- req_id: str
1289
- ) -> Optional[ThreadsFile]:
1265
+ def load_archived_threads(repo_root: Path, package_id: str, req_id: str) -> Optional[ThreadsFile]:
1290
1266
  """
1291
1267
  Load threads for a requirement from an archived package.
1292
1268
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: elspais
3
- Version: 0.11.1
3
+ Version: 0.11.2
4
4
  Summary: Requirements validation and traceability tools - L-Space connects all libraries
5
5
  Project-URL: Homepage, https://github.com/anspar/elspais
6
6
  Project-URL: Documentation, https://github.com/anspar/elspais#readme