elspais 0.11.0__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 +75 -23
  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.0.dist-info → elspais-0.11.2.dist-info}/METADATA +12 -9
  50. {elspais-0.11.0.dist-info → elspais-0.11.2.dist-info}/RECORD +53 -53
  51. {elspais-0.11.0.dist-info → elspais-0.11.2.dist-info}/WHEEL +0 -0
  52. {elspais-0.11.0.dist-info → elspais-0.11.2.dist-info}/entry_points.txt +0 -0
  53. {elspais-0.11.0.dist-info → elspais-0.11.2.dist-info}/licenses/LICENSE +0 -0
@@ -16,12 +16,12 @@ from typing import Any, Dict, List, Optional, Tuple
16
16
 
17
17
  from .models import CommentPosition, PositionType
18
18
 
19
-
20
19
  # =============================================================================
21
20
  # Enums
22
21
  # REQ-tv-d00012-B: Confidence levels as string enums
23
22
  # =============================================================================
24
23
 
24
+
25
25
  class ResolutionConfidence(str, Enum):
26
26
  """
27
27
  Confidence level for resolved position.
@@ -29,6 +29,7 @@ class ResolutionConfidence(str, Enum):
29
29
  REQ-tv-d00012-B: EXACT (hash matches), APPROXIMATE (fallback matched),
30
30
  or UNANCHORED (no match found).
31
31
  """
32
+
32
33
  EXACT = "exact"
33
34
  APPROXIMATE = "approximate"
34
35
  UNANCHORED = "unanchored"
@@ -38,6 +39,7 @@ class ResolutionConfidence(str, Enum):
38
39
  # Data Classes
39
40
  # =============================================================================
40
41
 
42
+
41
43
  @dataclass
42
44
  class ResolvedPosition:
43
45
  """
@@ -49,6 +51,7 @@ class ResolvedPosition:
49
51
  Contains all information needed to display a comment at its resolved
50
52
  location, including confidence level and original position for reference.
51
53
  """
54
+
52
55
  type: str # Resolved position type (PositionType value)
53
56
  confidence: str # ResolutionConfidence value
54
57
  lineNumber: Optional[int] # Resolved line (1-based), None for general
@@ -66,8 +69,8 @@ class ResolvedPosition:
66
69
  line_range: Optional[Tuple[int, int]],
67
70
  char_range: Optional[Tuple[int, int]],
68
71
  matched_text: Optional[str],
69
- original: CommentPosition
70
- ) -> 'ResolvedPosition':
72
+ original: CommentPosition,
73
+ ) -> "ResolvedPosition":
71
74
  """
72
75
  Factory for exact resolution (hash matched).
73
76
 
@@ -81,7 +84,7 @@ class ResolvedPosition:
81
84
  charRange=char_range,
82
85
  matchedText=matched_text,
83
86
  originalPosition=original,
84
- resolutionPath="hash_match"
87
+ resolutionPath="hash_match",
85
88
  )
86
89
 
87
90
  @classmethod
@@ -93,8 +96,8 @@ class ResolvedPosition:
93
96
  char_range: Optional[Tuple[int, int]],
94
97
  matched_text: Optional[str],
95
98
  original: CommentPosition,
96
- resolution_path: str
97
- ) -> 'ResolvedPosition':
99
+ resolution_path: str,
100
+ ) -> "ResolvedPosition":
98
101
  """
99
102
  Factory for approximate resolution (fallback succeeded).
100
103
 
@@ -108,11 +111,11 @@ class ResolvedPosition:
108
111
  charRange=char_range,
109
112
  matchedText=matched_text,
110
113
  originalPosition=original,
111
- resolutionPath=resolution_path
114
+ resolutionPath=resolution_path,
112
115
  )
113
116
 
114
117
  @classmethod
115
- def create_unanchored(cls, original: CommentPosition) -> 'ResolvedPosition':
118
+ def create_unanchored(cls, original: CommentPosition) -> "ResolvedPosition":
116
119
  """
117
120
  Factory for unanchored resolution (all fallbacks failed).
118
121
 
@@ -127,7 +130,7 @@ class ResolvedPosition:
127
130
  charRange=None,
128
131
  matchedText=None,
129
132
  originalPosition=original,
130
- resolutionPath="fallback_exhausted"
133
+ resolutionPath="fallback_exhausted",
131
134
  )
132
135
 
133
136
  def validate(self) -> Tuple[bool, List[str]]:
@@ -163,39 +166,39 @@ class ResolvedPosition:
163
166
  def to_dict(self) -> Dict[str, Any]:
164
167
  """Convert to JSON-serializable dictionary"""
165
168
  result: Dict[str, Any] = {
166
- 'type': self.type,
167
- 'confidence': self.confidence,
168
- 'resolutionPath': self.resolutionPath,
169
- 'originalPosition': self.originalPosition.to_dict()
169
+ "type": self.type,
170
+ "confidence": self.confidence,
171
+ "resolutionPath": self.resolutionPath,
172
+ "originalPosition": self.originalPosition.to_dict(),
170
173
  }
171
174
  if self.lineNumber is not None:
172
- result['lineNumber'] = self.lineNumber
175
+ result["lineNumber"] = self.lineNumber
173
176
  if self.lineRange is not None:
174
- result['lineRange'] = list(self.lineRange)
177
+ result["lineRange"] = list(self.lineRange)
175
178
  if self.charRange is not None:
176
- result['charRange'] = list(self.charRange)
179
+ result["charRange"] = list(self.charRange)
177
180
  if self.matchedText is not None:
178
- result['matchedText'] = self.matchedText
181
+ result["matchedText"] = self.matchedText
179
182
  return result
180
183
 
181
184
  @classmethod
182
- def from_dict(cls, data: Dict[str, Any]) -> 'ResolvedPosition':
185
+ def from_dict(cls, data: Dict[str, Any]) -> "ResolvedPosition":
183
186
  """Create from dictionary (JSON deserialization)"""
184
- line_range = data.get('lineRange')
187
+ line_range = data.get("lineRange")
185
188
  if line_range is not None:
186
189
  line_range = tuple(line_range)
187
- char_range = data.get('charRange')
190
+ char_range = data.get("charRange")
188
191
  if char_range is not None:
189
192
  char_range = tuple(char_range)
190
193
  return cls(
191
- type=data['type'],
192
- confidence=data['confidence'],
193
- lineNumber=data.get('lineNumber'),
194
+ type=data["type"],
195
+ confidence=data["confidence"],
196
+ lineNumber=data.get("lineNumber"),
194
197
  lineRange=line_range,
195
198
  charRange=char_range,
196
- matchedText=data.get('matchedText'),
197
- originalPosition=CommentPosition.from_dict(data['originalPosition']),
198
- resolutionPath=data.get('resolutionPath', 'unknown')
199
+ matchedText=data.get("matchedText"),
200
+ originalPosition=CommentPosition.from_dict(data["originalPosition"]),
201
+ resolutionPath=data.get("resolutionPath", "unknown"),
199
202
  )
200
203
 
201
204
 
@@ -203,6 +206,7 @@ class ResolvedPosition:
203
206
  # Helper Functions
204
207
  # =============================================================================
205
208
 
209
+
206
210
  def find_line_in_text(text: str, line_number: int) -> Optional[Tuple[int, int]]:
207
211
  """
208
212
  Find character range for a specific line in text.
@@ -218,7 +222,7 @@ def find_line_in_text(text: str, line_number: int) -> Optional[Tuple[int, int]]:
218
222
  if not text or line_number < 1:
219
223
  return None
220
224
 
221
- lines = text.split('\n')
225
+ lines = text.split("\n")
222
226
 
223
227
  if line_number > len(lines):
224
228
  return None
@@ -255,11 +259,7 @@ def find_context_in_text(text: str, context: str) -> Optional[Tuple[int, int]]:
255
259
  return (index, index + len(context))
256
260
 
257
261
 
258
- def find_keyword_occurrence(
259
- text: str,
260
- keyword: str,
261
- occurrence: int
262
- ) -> Optional[Tuple[int, int]]:
262
+ def find_keyword_occurrence(text: str, keyword: str, occurrence: int) -> Optional[Tuple[int, int]]:
263
263
  """
264
264
  Find character range of the Nth occurrence of a keyword.
265
265
 
@@ -312,20 +312,16 @@ def get_line_number_from_char_offset(text: str, char_offset: int) -> int:
312
312
  char_offset = min(char_offset, len(text) - 1)
313
313
 
314
314
  # Count newlines before offset
315
- newline_count = text[:char_offset + 1].count('\n')
315
+ newline_count = text[: char_offset + 1].count("\n")
316
316
 
317
317
  # Special case: if offset is exactly on a newline, it belongs to previous line
318
- if char_offset < len(text) and text[char_offset] == '\n':
318
+ if char_offset < len(text) and text[char_offset] == "\n":
319
319
  return newline_count # Don't add 1 since we're ON the newline
320
320
 
321
321
  return newline_count + 1
322
322
 
323
323
 
324
- def get_line_range_from_char_range(
325
- text: str,
326
- start: int,
327
- end: int
328
- ) -> Tuple[int, int]:
324
+ def get_line_range_from_char_range(text: str, start: int, end: int) -> Tuple[int, int]:
329
325
  """
330
326
  Convert character range to line range.
331
327
 
@@ -359,17 +355,16 @@ def get_total_lines(text: str) -> int:
359
355
  """
360
356
  if not text:
361
357
  return 0
362
- return text.count('\n') + 1
358
+ return text.count("\n") + 1
363
359
 
364
360
 
365
361
  # =============================================================================
366
362
  # Core Resolution Functions
367
363
  # =============================================================================
368
364
 
365
+
369
366
  def resolve_position(
370
- position: CommentPosition,
371
- content: str,
372
- current_hash: str
367
+ position: CommentPosition, content: str, current_hash: str
373
368
  ) -> ResolvedPosition:
374
369
  """
375
370
  Resolve a comment position against current requirement content.
@@ -414,10 +409,7 @@ def resolve_position(
414
409
  return _resolve_with_fallback(position, content)
415
410
 
416
411
 
417
- def _resolve_general(
418
- position: CommentPosition,
419
- content: str
420
- ) -> ResolvedPosition:
412
+ def _resolve_general(position: CommentPosition, content: str) -> ResolvedPosition:
421
413
  """
422
414
  Resolve GENERAL position type.
423
415
 
@@ -431,14 +423,11 @@ def _resolve_general(
431
423
  line_range=(1, total_lines) if total_lines > 0 else None,
432
424
  char_range=(0, len(content)) if content else None,
433
425
  matched_text=None,
434
- original=position
426
+ original=position,
435
427
  )
436
428
 
437
429
 
438
- def _resolve_exact(
439
- position: CommentPosition,
440
- content: str
441
- ) -> ResolvedPosition:
430
+ def _resolve_exact(position: CommentPosition, content: str) -> ResolvedPosition:
442
431
  """
443
432
  Resolve position when hash matches (exact confidence).
444
433
 
@@ -458,7 +447,7 @@ def _resolve_exact(
458
447
  matched_text = None
459
448
 
460
449
  if char_range:
461
- matched_text = content[char_range[0]:char_range[1]]
450
+ matched_text = content[char_range[0] : char_range[1]]
462
451
 
463
452
  return ResolvedPosition.create_exact(
464
453
  position_type=pos_type,
@@ -466,7 +455,7 @@ def _resolve_exact(
466
455
  line_range=(line_num, line_num) if line_num else None,
467
456
  char_range=char_range,
468
457
  matched_text=matched_text,
469
- original=position
458
+ original=position,
470
459
  )
471
460
 
472
461
  elif pos_type == PositionType.BLOCK.value:
@@ -479,7 +468,7 @@ def _resolve_exact(
479
468
  matched_text = None
480
469
  if start_range and end_range:
481
470
  char_range = (start_range[0], end_range[1])
482
- matched_text = content[char_range[0]:char_range[1]]
471
+ matched_text = content[char_range[0] : char_range[1]]
483
472
 
484
473
  return ResolvedPosition.create_exact(
485
474
  position_type=pos_type,
@@ -487,7 +476,7 @@ def _resolve_exact(
487
476
  line_range=line_range,
488
477
  char_range=char_range,
489
478
  matched_text=matched_text,
490
- original=position
479
+ original=position,
491
480
  )
492
481
  else:
493
482
  return ResolvedPosition.create_unanchored(position)
@@ -515,7 +504,7 @@ def _resolve_exact(
515
504
  line_range=line_range_result,
516
505
  char_range=char_range,
517
506
  matched_text=matched_text,
518
- original=position
507
+ original=position,
519
508
  )
520
509
  else:
521
510
  return ResolvedPosition.create_unanchored(position)
@@ -524,10 +513,7 @@ def _resolve_exact(
524
513
  return ResolvedPosition.create_unanchored(position)
525
514
 
526
515
 
527
- def _resolve_with_fallback(
528
- position: CommentPosition,
529
- content: str
530
- ) -> ResolvedPosition:
516
+ def _resolve_with_fallback(position: CommentPosition, content: str) -> ResolvedPosition:
531
517
  """
532
518
  Resolve position when hash differs (approximate confidence).
533
519
 
@@ -554,7 +540,7 @@ def _resolve_with_fallback(
554
540
  if 1 <= line_to_try <= total_lines:
555
541
  char_range = find_line_in_text(content, line_to_try)
556
542
  if char_range:
557
- matched_text = content[char_range[0]:char_range[1]]
543
+ matched_text = content[char_range[0] : char_range[1]]
558
544
  return ResolvedPosition.create_approximate(
559
545
  position_type=PositionType.LINE.value,
560
546
  line_number=line_to_try,
@@ -562,7 +548,7 @@ def _resolve_with_fallback(
562
548
  char_range=char_range,
563
549
  matched_text=matched_text,
564
550
  original=position,
565
- resolution_path="fallback_line_number"
551
+ resolution_path="fallback_line_number",
566
552
  )
567
553
 
568
554
  # Strategy 2: Try fallbackContext
@@ -571,9 +557,7 @@ def _resolve_with_fallback(
571
557
  char_range = find_context_in_text(content, position.fallbackContext)
572
558
  if char_range:
573
559
  line_number = get_line_number_from_char_offset(content, char_range[0])
574
- line_range = get_line_range_from_char_range(
575
- content, char_range[0], char_range[1]
576
- )
560
+ line_range = get_line_range_from_char_range(content, char_range[0], char_range[1])
577
561
  return ResolvedPosition.create_approximate(
578
562
  position_type=PositionType.LINE.value, # Resolved to line
579
563
  line_number=line_number,
@@ -581,7 +565,7 @@ def _resolve_with_fallback(
581
565
  char_range=char_range,
582
566
  matched_text=position.fallbackContext,
583
567
  original=position,
584
- resolution_path="fallback_context"
568
+ resolution_path="fallback_context",
585
569
  )
586
570
 
587
571
  # Strategy 3: Try keyword occurrence
@@ -591,9 +575,7 @@ def _resolve_with_fallback(
591
575
  char_range = find_keyword_occurrence(content, position.keyword, occurrence)
592
576
  if char_range:
593
577
  line_number = get_line_number_from_char_offset(content, char_range[0])
594
- line_range = get_line_range_from_char_range(
595
- content, char_range[0], char_range[1]
596
- )
578
+ line_range = get_line_range_from_char_range(content, char_range[0], char_range[1])
597
579
  return ResolvedPosition.create_approximate(
598
580
  position_type=PositionType.WORD.value,
599
581
  line_number=line_number,
@@ -601,7 +583,7 @@ def _resolve_with_fallback(
601
583
  char_range=char_range,
602
584
  matched_text=position.keyword,
603
585
  original=position,
604
- resolution_path="fallback_keyword"
586
+ resolution_path="fallback_keyword",
605
587
  )
606
588
 
607
589
  # Strategy 4: Fall back to general (unanchored)