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.
- elspais/__init__.py +1 -1
- elspais/cli.py +29 -10
- elspais/commands/analyze.py +5 -6
- elspais/commands/changed.py +2 -6
- elspais/commands/config_cmd.py +4 -4
- elspais/commands/edit.py +32 -36
- elspais/commands/hash_cmd.py +24 -18
- elspais/commands/index.py +8 -7
- elspais/commands/init.py +4 -4
- elspais/commands/reformat_cmd.py +32 -43
- elspais/commands/rules_cmd.py +6 -2
- elspais/commands/trace.py +23 -19
- elspais/commands/validate.py +8 -10
- elspais/config/defaults.py +7 -1
- elspais/core/content_rules.py +0 -1
- elspais/core/git.py +4 -10
- elspais/core/parser.py +55 -56
- elspais/core/patterns.py +2 -6
- elspais/core/rules.py +10 -15
- elspais/mcp/__init__.py +2 -0
- elspais/mcp/context.py +1 -0
- elspais/mcp/serializers.py +1 -1
- elspais/mcp/server.py +54 -39
- elspais/reformat/__init__.py +13 -13
- elspais/reformat/detector.py +9 -16
- elspais/reformat/hierarchy.py +8 -7
- elspais/reformat/line_breaks.py +36 -38
- elspais/reformat/prompts.py +22 -12
- elspais/reformat/transformer.py +43 -41
- elspais/sponsors/__init__.py +0 -2
- elspais/testing/__init__.py +1 -1
- elspais/testing/result_parser.py +25 -21
- elspais/trace_view/__init__.py +4 -3
- elspais/trace_view/coverage.py +5 -5
- elspais/trace_view/generators/__init__.py +1 -1
- elspais/trace_view/generators/base.py +17 -12
- elspais/trace_view/generators/csv.py +2 -6
- elspais/trace_view/generators/markdown.py +3 -8
- elspais/trace_view/html/__init__.py +4 -2
- elspais/trace_view/html/generator.py +423 -289
- elspais/trace_view/models.py +25 -0
- elspais/trace_view/review/__init__.py +21 -18
- elspais/trace_view/review/branches.py +114 -121
- elspais/trace_view/review/models.py +232 -237
- elspais/trace_view/review/position.py +53 -71
- elspais/trace_view/review/server.py +264 -288
- elspais/trace_view/review/status.py +43 -58
- elspais/trace_view/review/storage.py +48 -72
- {elspais-0.11.1.dist-info → elspais-0.11.2.dist-info}/METADATA +1 -1
- {elspais-0.11.1.dist-info → elspais-0.11.2.dist-info}/RECORD +53 -53
- {elspais-0.11.1.dist-info → elspais-0.11.2.dist-info}/WHEEL +0 -0
- {elspais-0.11.1.dist-info → elspais-0.11.2.dist-info}/entry_points.txt +0 -0
- {elspais-0.11.1.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
|
-
) ->
|
|
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
|
-
) ->
|
|
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) ->
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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[
|
|
175
|
+
result["lineNumber"] = self.lineNumber
|
|
173
176
|
if self.lineRange is not None:
|
|
174
|
-
result[
|
|
177
|
+
result["lineRange"] = list(self.lineRange)
|
|
175
178
|
if self.charRange is not None:
|
|
176
|
-
result[
|
|
179
|
+
result["charRange"] = list(self.charRange)
|
|
177
180
|
if self.matchedText is not None:
|
|
178
|
-
result[
|
|
181
|
+
result["matchedText"] = self.matchedText
|
|
179
182
|
return result
|
|
180
183
|
|
|
181
184
|
@classmethod
|
|
182
|
-
def from_dict(cls, data: Dict[str, Any]) ->
|
|
185
|
+
def from_dict(cls, data: Dict[str, Any]) -> "ResolvedPosition":
|
|
183
186
|
"""Create from dictionary (JSON deserialization)"""
|
|
184
|
-
line_range = data.get(
|
|
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(
|
|
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[
|
|
192
|
-
confidence=data[
|
|
193
|
-
lineNumber=data.get(
|
|
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(
|
|
197
|
-
originalPosition=CommentPosition.from_dict(data[
|
|
198
|
-
resolutionPath=data.get(
|
|
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(
|
|
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(
|
|
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] ==
|
|
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(
|
|
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)
|