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.
- elspais/__init__.py +1 -1
- elspais/cli.py +75 -23
- 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.0.dist-info → elspais-0.11.2.dist-info}/METADATA +12 -9
- {elspais-0.11.0.dist-info → elspais-0.11.2.dist-info}/RECORD +53 -53
- {elspais-0.11.0.dist-info → elspais-0.11.2.dist-info}/WHEEL +0 -0
- {elspais-0.11.0.dist-info → elspais-0.11.2.dist-info}/entry_points.txt +0 -0
- {elspais-0.11.0.dist-info → elspais-0.11.2.dist-info}/licenses/LICENSE +0 -0
elspais/reformat/line_breaks.py
CHANGED
|
@@ -26,7 +26,7 @@ def normalize_line_breaks(content: str, reflow: bool = True) -> str:
|
|
|
26
26
|
Returns:
|
|
27
27
|
Content with normalized line breaks
|
|
28
28
|
"""
|
|
29
|
-
lines = content.split(
|
|
29
|
+
lines = content.split("\n")
|
|
30
30
|
result_lines: List[str] = []
|
|
31
31
|
|
|
32
32
|
i = 0
|
|
@@ -34,14 +34,14 @@ def normalize_line_breaks(content: str, reflow: bool = True) -> str:
|
|
|
34
34
|
line = lines[i]
|
|
35
35
|
|
|
36
36
|
# Check if this is a section header (## Something)
|
|
37
|
-
if re.match(r
|
|
37
|
+
if re.match(r"^##\s+\w", line):
|
|
38
38
|
result_lines.append(line)
|
|
39
39
|
# Skip blank lines immediately after section header
|
|
40
40
|
i += 1
|
|
41
|
-
while i < len(lines) and lines[i].strip() ==
|
|
41
|
+
while i < len(lines) and lines[i].strip() == "":
|
|
42
42
|
i += 1
|
|
43
43
|
# Add single blank line after header for readability
|
|
44
|
-
result_lines.append(
|
|
44
|
+
result_lines.append("")
|
|
45
45
|
continue
|
|
46
46
|
|
|
47
47
|
# Check if this starts a paragraph that might need reflowing
|
|
@@ -52,9 +52,11 @@ def normalize_line_breaks(content: str, reflow: bool = True) -> str:
|
|
|
52
52
|
while i < len(lines):
|
|
53
53
|
next_line = lines[i]
|
|
54
54
|
# Stop at blank lines, structural elements, or next section
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
if (
|
|
56
|
+
next_line.strip() == ""
|
|
57
|
+
or _is_structural_line(next_line)
|
|
58
|
+
or re.match(r"^##\s+", next_line)
|
|
59
|
+
):
|
|
58
60
|
break
|
|
59
61
|
para_lines.append(next_line.rstrip())
|
|
60
62
|
i += 1
|
|
@@ -69,7 +71,7 @@ def normalize_line_breaks(content: str, reflow: bool = True) -> str:
|
|
|
69
71
|
i += 1
|
|
70
72
|
|
|
71
73
|
# Clean up multiple consecutive blank lines
|
|
72
|
-
return _collapse_blank_lines(
|
|
74
|
+
return _collapse_blank_lines("\n".join(result_lines))
|
|
73
75
|
|
|
74
76
|
|
|
75
77
|
def _is_structural_line(line: str) -> bool:
|
|
@@ -89,35 +91,35 @@ def _is_structural_line(line: str) -> bool:
|
|
|
89
91
|
return False
|
|
90
92
|
|
|
91
93
|
# Headers
|
|
92
|
-
if stripped.startswith(
|
|
94
|
+
if stripped.startswith("#"):
|
|
93
95
|
return True
|
|
94
96
|
|
|
95
97
|
# Lettered assertions (A. B. C. etc)
|
|
96
|
-
if re.match(r
|
|
98
|
+
if re.match(r"^[A-Z]\.\s", stripped):
|
|
97
99
|
return True
|
|
98
100
|
|
|
99
101
|
# Numbered lists (1. 2. 3. etc)
|
|
100
|
-
if re.match(r
|
|
102
|
+
if re.match(r"^\d+\.\s", stripped):
|
|
101
103
|
return True
|
|
102
104
|
|
|
103
105
|
# Bullet points
|
|
104
|
-
if stripped.startswith((
|
|
106
|
+
if stripped.startswith(("- ", "* ", "+ ")):
|
|
105
107
|
return True
|
|
106
108
|
|
|
107
109
|
# Metadata line
|
|
108
|
-
if stripped.startswith(
|
|
110
|
+
if stripped.startswith("**Level**:") or stripped.startswith("**Status**:"):
|
|
109
111
|
return True
|
|
110
112
|
|
|
111
113
|
# Combined metadata line
|
|
112
|
-
if re.match(r
|
|
114
|
+
if re.match(r"\*\*Level\*\*:", stripped):
|
|
113
115
|
return True
|
|
114
116
|
|
|
115
117
|
# End marker
|
|
116
|
-
if stripped.startswith(
|
|
118
|
+
if stripped.startswith("*End*"):
|
|
117
119
|
return True
|
|
118
120
|
|
|
119
121
|
# Code fence
|
|
120
|
-
if stripped.startswith(
|
|
122
|
+
if stripped.startswith("```"):
|
|
121
123
|
return True
|
|
122
124
|
|
|
123
125
|
return False
|
|
@@ -134,15 +136,15 @@ def _reflow_paragraph(lines: List[str]) -> str:
|
|
|
134
136
|
Single reflowed line
|
|
135
137
|
"""
|
|
136
138
|
if not lines:
|
|
137
|
-
return
|
|
139
|
+
return ""
|
|
138
140
|
|
|
139
141
|
if len(lines) == 1:
|
|
140
142
|
return lines[0]
|
|
141
143
|
|
|
142
144
|
# Join lines with space, collapsing multiple spaces
|
|
143
|
-
joined =
|
|
145
|
+
joined = " ".join(line.strip() for line in lines if line.strip())
|
|
144
146
|
# Collapse multiple spaces
|
|
145
|
-
return re.sub(r
|
|
147
|
+
return re.sub(r"\s+", " ", joined)
|
|
146
148
|
|
|
147
149
|
|
|
148
150
|
def _collapse_blank_lines(content: str) -> str:
|
|
@@ -156,14 +158,10 @@ def _collapse_blank_lines(content: str) -> str:
|
|
|
156
158
|
Content with at most one blank line between paragraphs
|
|
157
159
|
"""
|
|
158
160
|
# Replace 3+ newlines with 2 newlines (one blank line)
|
|
159
|
-
return re.sub(r
|
|
161
|
+
return re.sub(r"\n{3,}", "\n\n", content)
|
|
160
162
|
|
|
161
163
|
|
|
162
|
-
def fix_requirement_line_breaks(
|
|
163
|
-
body: str,
|
|
164
|
-
rationale: str,
|
|
165
|
-
reflow: bool = True
|
|
166
|
-
) -> Tuple[str, str]:
|
|
164
|
+
def fix_requirement_line_breaks(body: str, rationale: str, reflow: bool = True) -> Tuple[str, str]:
|
|
167
165
|
"""
|
|
168
166
|
Fix line breaks in requirement body and rationale.
|
|
169
167
|
|
|
@@ -175,8 +173,8 @@ def fix_requirement_line_breaks(
|
|
|
175
173
|
Returns:
|
|
176
174
|
Tuple of (fixed_body, fixed_rationale)
|
|
177
175
|
"""
|
|
178
|
-
fixed_body = normalize_line_breaks(body, reflow=reflow) if body else
|
|
179
|
-
fixed_rationale = normalize_line_breaks(rationale, reflow=reflow) if rationale else
|
|
176
|
+
fixed_body = normalize_line_breaks(body, reflow=reflow) if body else ""
|
|
177
|
+
fixed_rationale = normalize_line_breaks(rationale, reflow=reflow) if rationale else ""
|
|
180
178
|
|
|
181
179
|
return fixed_body, fixed_rationale
|
|
182
180
|
|
|
@@ -188,15 +186,15 @@ def detect_line_break_issues(content: str) -> List[str]:
|
|
|
188
186
|
Returns list of issues found for reporting.
|
|
189
187
|
"""
|
|
190
188
|
issues = []
|
|
191
|
-
lines = content.split(
|
|
189
|
+
lines = content.split("\n")
|
|
192
190
|
|
|
193
191
|
for i, line in enumerate(lines):
|
|
194
192
|
# Check for blank line after section header
|
|
195
|
-
if re.match(r
|
|
193
|
+
if re.match(r"^##\s+\w", line):
|
|
196
194
|
# Look ahead for multiple blank lines
|
|
197
195
|
blank_count = 0
|
|
198
196
|
j = i + 1
|
|
199
|
-
while j < len(lines) and lines[j].strip() ==
|
|
197
|
+
while j < len(lines) and lines[j].strip() == "":
|
|
200
198
|
blank_count += 1
|
|
201
199
|
j += 1
|
|
202
200
|
if blank_count > 1:
|
|
@@ -206,15 +204,15 @@ def detect_line_break_issues(content: str) -> List[str]:
|
|
|
206
204
|
|
|
207
205
|
# Check for mid-sentence line break (line ends without punctuation)
|
|
208
206
|
stripped = line.rstrip()
|
|
209
|
-
if (
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
207
|
+
if (
|
|
208
|
+
stripped
|
|
209
|
+
and not _is_structural_line(line)
|
|
210
|
+
and i + 1 < len(lines)
|
|
211
|
+
and lines[i + 1].strip()
|
|
212
|
+
and not _is_structural_line(lines[i + 1])
|
|
213
|
+
):
|
|
214
214
|
# Line ends with a word (not punctuation), followed by non-empty line
|
|
215
215
|
if stripped and stripped[-1].isalnum():
|
|
216
|
-
issues.append(
|
|
217
|
-
f"Line {i+1}: Possible mid-sentence line break"
|
|
218
|
-
)
|
|
216
|
+
issues.append(f"Line {i+1}: Possible mid-sentence line break")
|
|
219
217
|
|
|
220
218
|
return issues
|
elspais/reformat/prompts.py
CHANGED
|
@@ -14,23 +14,31 @@ JSON_SCHEMA = {
|
|
|
14
14
|
"properties": {
|
|
15
15
|
"rationale": {
|
|
16
16
|
"type": "string",
|
|
17
|
-
"description":
|
|
17
|
+
"description": (
|
|
18
|
+
"Non-normative context explaining why this requirement exists. "
|
|
19
|
+
"No SHALL/MUST language."
|
|
20
|
+
),
|
|
18
21
|
},
|
|
19
22
|
"assertions": {
|
|
20
23
|
"type": "array",
|
|
21
24
|
"items": {"type": "string"},
|
|
22
|
-
"description":
|
|
23
|
-
|
|
25
|
+
"description": (
|
|
26
|
+
"List of assertions, each starting with 'The system SHALL...' "
|
|
27
|
+
"or similar prescriptive language."
|
|
28
|
+
),
|
|
29
|
+
},
|
|
24
30
|
},
|
|
25
|
-
"required": ["rationale", "assertions"]
|
|
31
|
+
"required": ["rationale", "assertions"],
|
|
26
32
|
}
|
|
27
33
|
|
|
28
|
-
JSON_SCHEMA_STR = json.dumps(JSON_SCHEMA, separators=(
|
|
34
|
+
JSON_SCHEMA_STR = json.dumps(JSON_SCHEMA, separators=(",", ":"))
|
|
29
35
|
|
|
30
36
|
# System prompt for requirement reformatting
|
|
31
|
-
REFORMAT_SYSTEM_PROMPT = """You are a requirements engineering expert specializing in
|
|
37
|
+
REFORMAT_SYSTEM_PROMPT = """You are a requirements engineering expert specializing in \
|
|
38
|
+
FDA 21 CFR Part 11 compliant clinical trial systems.
|
|
32
39
|
|
|
33
|
-
Your task is to reformat requirements from an old descriptive format to a new
|
|
40
|
+
Your task is to reformat requirements from an old descriptive format to a new \
|
|
41
|
+
prescriptive assertion-based format.
|
|
34
42
|
|
|
35
43
|
EXTRACTION RULES:
|
|
36
44
|
1. Extract ALL obligations from the old format (body text, bullet points, acceptance criteria)
|
|
@@ -47,7 +55,7 @@ RATIONALE RULES:
|
|
|
47
55
|
1. The rationale provides context for WHY this requirement exists
|
|
48
56
|
2. Rationale MUST NOT introduce new obligations
|
|
49
57
|
3. Rationale MUST NOT use SHALL/MUST language
|
|
50
|
-
4. Rationale can explain regulatory context, design decisions, or relationships
|
|
58
|
+
4. Rationale can explain regulatory context, design decisions, or relationships
|
|
51
59
|
|
|
52
60
|
LANGUAGE GUIDELINES:
|
|
53
61
|
- Use "The system SHALL..." for system behaviors
|
|
@@ -59,11 +67,12 @@ LANGUAGE GUIDELINES:
|
|
|
59
67
|
OUTPUT FORMAT:
|
|
60
68
|
Return a JSON object with:
|
|
61
69
|
- "rationale": A paragraph explaining the requirement's purpose (no SHALL language)
|
|
62
|
-
- "assertions": An array of strings, each
|
|
70
|
+
- "assertions": An array of strings, each a complete assertion with subject and SHALL
|
|
63
71
|
|
|
64
72
|
Example output:
|
|
65
73
|
{
|
|
66
|
-
"rationale": "This requirement ensures complete audit trails for regulatory compliance.
|
|
74
|
+
"rationale": "This requirement ensures complete audit trails for regulatory compliance. \
|
|
75
|
+
FDA 21 CFR Part 11 mandates tamper-evident histories of all modifications.",
|
|
67
76
|
"assertions": [
|
|
68
77
|
"The system SHALL store all data changes as immutable events.",
|
|
69
78
|
"The system SHALL preserve the complete history of all modifications.",
|
|
@@ -80,7 +89,7 @@ def build_user_prompt(
|
|
|
80
89
|
status: str,
|
|
81
90
|
implements: list,
|
|
82
91
|
body: str,
|
|
83
|
-
rationale: str = ""
|
|
92
|
+
rationale: str = "",
|
|
84
93
|
) -> str:
|
|
85
94
|
"""
|
|
86
95
|
Build the user prompt for reformatting a requirement.
|
|
@@ -118,6 +127,7 @@ CURRENT RATIONALE:
|
|
|
118
127
|
"""
|
|
119
128
|
|
|
120
129
|
prompt += """
|
|
121
|
-
Extract all obligations and convert them to labeled assertions.
|
|
130
|
+
Extract all obligations and convert them to labeled assertions. \
|
|
131
|
+
Return ONLY the JSON object with "rationale" and "assertions" fields."""
|
|
122
132
|
|
|
123
133
|
return prompt
|
elspais/reformat/transformer.py
CHANGED
|
@@ -12,13 +12,11 @@ import sys
|
|
|
12
12
|
from typing import List, Optional, Tuple
|
|
13
13
|
|
|
14
14
|
from elspais.reformat.hierarchy import RequirementNode
|
|
15
|
-
from elspais.reformat.prompts import
|
|
15
|
+
from elspais.reformat.prompts import JSON_SCHEMA_STR, REFORMAT_SYSTEM_PROMPT, build_user_prompt
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def reformat_requirement(
|
|
19
|
-
node: RequirementNode,
|
|
20
|
-
model: str = "sonnet",
|
|
21
|
-
verbose: bool = False
|
|
19
|
+
node: RequirementNode, model: str = "sonnet", verbose: bool = False
|
|
22
20
|
) -> Tuple[Optional[dict], bool, str]:
|
|
23
21
|
"""
|
|
24
22
|
Use Claude CLI to reformat a requirement.
|
|
@@ -40,30 +38,32 @@ def reformat_requirement(
|
|
|
40
38
|
status=node.status,
|
|
41
39
|
implements=node.implements,
|
|
42
40
|
body=node.body,
|
|
43
|
-
rationale=node.rationale
|
|
41
|
+
rationale=node.rationale,
|
|
44
42
|
)
|
|
45
43
|
|
|
46
44
|
# Build the claude command
|
|
47
45
|
cmd = [
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
46
|
+
"claude",
|
|
47
|
+
"-p", # Print mode (non-interactive)
|
|
48
|
+
"--output-format",
|
|
49
|
+
"json",
|
|
50
|
+
"--json-schema",
|
|
51
|
+
JSON_SCHEMA_STR,
|
|
52
|
+
"--system-prompt",
|
|
53
|
+
REFORMAT_SYSTEM_PROMPT,
|
|
54
|
+
"--tools",
|
|
55
|
+
"", # Disable all tools
|
|
56
|
+
"--model",
|
|
57
|
+
model,
|
|
58
|
+
user_prompt,
|
|
56
59
|
]
|
|
57
60
|
|
|
58
61
|
if verbose:
|
|
59
|
-
print(
|
|
62
|
+
print(" Running: claude -p --output-format json ...", file=sys.stderr)
|
|
60
63
|
|
|
61
64
|
try:
|
|
62
65
|
result = subprocess.run(
|
|
63
|
-
cmd,
|
|
64
|
-
capture_output=True,
|
|
65
|
-
text=True,
|
|
66
|
-
timeout=120 # 2 minute timeout
|
|
66
|
+
cmd, capture_output=True, text=True, timeout=120 # 2 minute timeout
|
|
67
67
|
)
|
|
68
68
|
|
|
69
69
|
if result.returncode != 0:
|
|
@@ -105,29 +105,33 @@ def parse_claude_response(response: str) -> Optional[dict]:
|
|
|
105
105
|
data = json.loads(response)
|
|
106
106
|
|
|
107
107
|
# Check for error
|
|
108
|
-
if data.get(
|
|
108
|
+
if data.get("is_error") or data.get("subtype") == "error":
|
|
109
109
|
return None
|
|
110
110
|
|
|
111
111
|
# The structured output is in 'structured_output' field
|
|
112
|
-
if
|
|
113
|
-
structured = data[
|
|
114
|
-
if
|
|
112
|
+
if "structured_output" in data:
|
|
113
|
+
structured = data["structured_output"]
|
|
114
|
+
if (
|
|
115
|
+
isinstance(structured, dict)
|
|
116
|
+
and "rationale" in structured
|
|
117
|
+
and "assertions" in structured
|
|
118
|
+
):
|
|
115
119
|
return structured
|
|
116
120
|
|
|
117
121
|
# Fallback: Direct result (if schema not used)
|
|
118
|
-
if
|
|
122
|
+
if "rationale" in data and "assertions" in data:
|
|
119
123
|
return data
|
|
120
124
|
|
|
121
125
|
# Fallback: Wrapped in result field
|
|
122
|
-
if
|
|
123
|
-
result = data[
|
|
124
|
-
if isinstance(result, dict) and
|
|
126
|
+
if "result" in data:
|
|
127
|
+
result = data["result"]
|
|
128
|
+
if isinstance(result, dict) and "rationale" in result:
|
|
125
129
|
return result
|
|
126
130
|
# Result might be a JSON string
|
|
127
131
|
if isinstance(result, str) and result.strip():
|
|
128
132
|
try:
|
|
129
133
|
parsed = json.loads(result)
|
|
130
|
-
if
|
|
134
|
+
if "rationale" in parsed:
|
|
131
135
|
return parsed
|
|
132
136
|
except json.JSONDecodeError:
|
|
133
137
|
pass
|
|
@@ -137,13 +141,13 @@ def parse_claude_response(response: str) -> Optional[dict]:
|
|
|
137
141
|
except json.JSONDecodeError:
|
|
138
142
|
# Try to extract JSON from the response
|
|
139
143
|
try:
|
|
140
|
-
json_start = response.find(
|
|
141
|
-
json_end = response.rfind(
|
|
144
|
+
json_start = response.find("{")
|
|
145
|
+
json_end = response.rfind("}") + 1
|
|
142
146
|
if json_start >= 0 and json_end > json_start:
|
|
143
147
|
parsed = json.loads(response[json_start:json_end])
|
|
144
|
-
if
|
|
145
|
-
return parsed[
|
|
146
|
-
if
|
|
148
|
+
if "structured_output" in parsed:
|
|
149
|
+
return parsed["structured_output"]
|
|
150
|
+
if "rationale" in parsed and "assertions" in parsed:
|
|
147
151
|
return parsed
|
|
148
152
|
except json.JSONDecodeError:
|
|
149
153
|
pass
|
|
@@ -157,7 +161,7 @@ def assemble_new_format(
|
|
|
157
161
|
status: str,
|
|
158
162
|
implements: List[str],
|
|
159
163
|
rationale: str,
|
|
160
|
-
assertions: List[str]
|
|
164
|
+
assertions: List[str],
|
|
161
165
|
) -> str:
|
|
162
166
|
"""
|
|
163
167
|
Assemble the new format requirement markdown.
|
|
@@ -200,11 +204,11 @@ def assemble_new_format(
|
|
|
200
204
|
|
|
201
205
|
# Label assertions A, B, C, etc.
|
|
202
206
|
for i, assertion in enumerate(assertions):
|
|
203
|
-
label = chr(ord(
|
|
207
|
+
label = chr(ord("A") + i)
|
|
204
208
|
# Clean up assertion text
|
|
205
209
|
assertion_text = assertion.strip()
|
|
206
210
|
# Remove any existing label if present
|
|
207
|
-
if len(assertion_text) > 2 and assertion_text[1] ==
|
|
211
|
+
if len(assertion_text) > 2 and assertion_text[1] == "." and assertion_text[0].isupper():
|
|
208
212
|
assertion_text = assertion_text[2:].strip()
|
|
209
213
|
lines.append(f"{label}. {assertion_text}")
|
|
210
214
|
|
|
@@ -219,9 +223,7 @@ def assemble_new_format(
|
|
|
219
223
|
|
|
220
224
|
|
|
221
225
|
def validate_reformatted_content(
|
|
222
|
-
original: RequirementNode,
|
|
223
|
-
rationale: str,
|
|
224
|
-
assertions: List[str]
|
|
226
|
+
original: RequirementNode, rationale: str, assertions: List[str]
|
|
225
227
|
) -> Tuple[bool, List[str]]:
|
|
226
228
|
"""
|
|
227
229
|
Validate that reformatted content is well-formed.
|
|
@@ -243,12 +245,12 @@ def validate_reformatted_content(
|
|
|
243
245
|
|
|
244
246
|
# Check each assertion uses SHALL
|
|
245
247
|
for i, assertion in enumerate(assertions):
|
|
246
|
-
label = chr(ord(
|
|
247
|
-
if
|
|
248
|
+
label = chr(ord("A") + i)
|
|
249
|
+
if "SHALL" not in assertion.upper():
|
|
248
250
|
warnings.append(f"Assertion {label} missing SHALL keyword")
|
|
249
251
|
|
|
250
252
|
# Check rationale doesn't use SHALL
|
|
251
|
-
if
|
|
253
|
+
if "SHALL" in rationale.upper():
|
|
252
254
|
warnings.append("Rationale contains SHALL (should be non-normative)")
|
|
253
255
|
|
|
254
256
|
# Check assertion count
|
elspais/sponsors/__init__.py
CHANGED
|
@@ -69,8 +69,6 @@ def parse_yaml(content: str) -> Dict[str, Any]:
|
|
|
69
69
|
current_key: Optional[str] = None
|
|
70
70
|
current_list: Optional[List[Dict]] = None
|
|
71
71
|
current_dict: Optional[Dict[str, Any]] = None
|
|
72
|
-
list_key: Optional[str] = None
|
|
73
|
-
indent_stack: List[tuple] = [] # (indent_level, container)
|
|
74
72
|
|
|
75
73
|
lines = content.split("\n")
|
|
76
74
|
|
elspais/testing/__init__.py
CHANGED
|
@@ -11,7 +11,7 @@ This package provides test-to-requirement mapping and coverage analysis:
|
|
|
11
11
|
from elspais.testing.config import TestingConfig
|
|
12
12
|
from elspais.testing.mapper import RequirementTestData, TestMapper, TestMappingResult
|
|
13
13
|
from elspais.testing.result_parser import ResultParser, TestResult, TestStatus
|
|
14
|
-
from elspais.testing.scanner import TestReference,
|
|
14
|
+
from elspais.testing.scanner import TestReference, TestScanner, TestScanResult
|
|
15
15
|
|
|
16
16
|
__all__ = [
|
|
17
17
|
"TestingConfig",
|
elspais/testing/result_parser.py
CHANGED
|
@@ -10,7 +10,7 @@ import xml.etree.ElementTree as ET
|
|
|
10
10
|
from dataclasses import dataclass, field
|
|
11
11
|
from enum import Enum
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
from typing import
|
|
13
|
+
from typing import List, Optional, Set
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class TestStatus(Enum):
|
|
@@ -143,7 +143,7 @@ class ResultParser:
|
|
|
143
143
|
tree = ET.parse(file_path)
|
|
144
144
|
root = tree.getroot()
|
|
145
145
|
except ET.ParseError as e:
|
|
146
|
-
raise ValueError(f"Invalid XML: {e}")
|
|
146
|
+
raise ValueError(f"Invalid XML: {e}") from e
|
|
147
147
|
|
|
148
148
|
# Handle both <testsuites> and <testsuite> as root
|
|
149
149
|
if root.tag == "testsuites":
|
|
@@ -181,15 +181,17 @@ class ResultParser:
|
|
|
181
181
|
# Extract requirement IDs from test name
|
|
182
182
|
req_ids = self._extract_requirement_ids(test_name, classname)
|
|
183
183
|
|
|
184
|
-
results.append(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
184
|
+
results.append(
|
|
185
|
+
TestResult(
|
|
186
|
+
test_name=test_name,
|
|
187
|
+
classname=classname,
|
|
188
|
+
status=status,
|
|
189
|
+
requirement_ids=req_ids,
|
|
190
|
+
result_file=file_path,
|
|
191
|
+
duration=duration,
|
|
192
|
+
message=message,
|
|
193
|
+
)
|
|
194
|
+
)
|
|
193
195
|
|
|
194
196
|
return results
|
|
195
197
|
|
|
@@ -209,7 +211,7 @@ class ResultParser:
|
|
|
209
211
|
with open(file_path, encoding="utf-8") as f:
|
|
210
212
|
data = json.load(f)
|
|
211
213
|
except json.JSONDecodeError as e:
|
|
212
|
-
raise ValueError(f"Invalid JSON: {e}")
|
|
214
|
+
raise ValueError(f"Invalid JSON: {e}") from e
|
|
213
215
|
|
|
214
216
|
# Handle pytest-json-report format
|
|
215
217
|
tests = data.get("tests", [])
|
|
@@ -241,15 +243,17 @@ class ResultParser:
|
|
|
241
243
|
# Extract requirement IDs
|
|
242
244
|
req_ids = self._extract_requirement_ids(test_name, classname)
|
|
243
245
|
|
|
244
|
-
results.append(
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
246
|
+
results.append(
|
|
247
|
+
TestResult(
|
|
248
|
+
test_name=test_name,
|
|
249
|
+
classname=classname,
|
|
250
|
+
status=status,
|
|
251
|
+
requirement_ids=req_ids,
|
|
252
|
+
result_file=file_path,
|
|
253
|
+
duration=duration,
|
|
254
|
+
message=message,
|
|
255
|
+
)
|
|
256
|
+
)
|
|
253
257
|
|
|
254
258
|
return results
|
|
255
259
|
|
elspais/trace_view/__init__.py
CHANGED
|
@@ -13,8 +13,8 @@ Optional dependencies:
|
|
|
13
13
|
- pip install elspais[trace-review] for review server (requires flask)
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
-
from elspais.trace_view.models import TraceViewRequirement, TestInfo, GitChangeInfo
|
|
17
16
|
from elspais.trace_view.generators.base import TraceViewGenerator
|
|
17
|
+
from elspais.trace_view.models import GitChangeInfo, TestInfo, TraceViewRequirement
|
|
18
18
|
|
|
19
19
|
__all__ = [
|
|
20
20
|
"TraceViewRequirement",
|
|
@@ -30,12 +30,14 @@ __all__ = [
|
|
|
30
30
|
def generate_markdown(requirements, **kwargs):
|
|
31
31
|
"""Generate Markdown traceability matrix."""
|
|
32
32
|
from elspais.trace_view.generators.markdown import generate_markdown as _gen
|
|
33
|
+
|
|
33
34
|
return _gen(requirements, **kwargs)
|
|
34
35
|
|
|
35
36
|
|
|
36
37
|
def generate_csv(requirements, **kwargs):
|
|
37
38
|
"""Generate CSV traceability matrix."""
|
|
38
39
|
from elspais.trace_view.generators.csv import generate_csv as _gen
|
|
40
|
+
|
|
39
41
|
return _gen(requirements, **kwargs)
|
|
40
42
|
|
|
41
43
|
|
|
@@ -48,7 +50,6 @@ def generate_html(requirements, **kwargs):
|
|
|
48
50
|
from elspais.trace_view.html import HTMLGenerator
|
|
49
51
|
except ImportError as e:
|
|
50
52
|
raise ImportError(
|
|
51
|
-
"HTML generation requires Jinja2. "
|
|
52
|
-
"Install with: pip install elspais[trace-view]"
|
|
53
|
+
"HTML generation requires Jinja2. " "Install with: pip install elspais[trace-view]"
|
|
53
54
|
) from e
|
|
54
55
|
return HTMLGenerator(requirements, **kwargs).generate()
|
elspais/trace_view/coverage.py
CHANGED
|
@@ -5,7 +5,7 @@ Provides functions to calculate implementation coverage and status
|
|
|
5
5
|
for requirements.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from typing import Dict, List
|
|
8
|
+
from typing import Dict, List
|
|
9
9
|
|
|
10
10
|
from elspais.trace_view.models import TraceViewRequirement
|
|
11
11
|
|
|
@@ -124,9 +124,7 @@ def get_implementation_status(requirements: ReqDict, req_id: str) -> str:
|
|
|
124
124
|
return "Partial"
|
|
125
125
|
|
|
126
126
|
|
|
127
|
-
def generate_coverage_report(
|
|
128
|
-
requirements: ReqDict, get_status_fn=None
|
|
129
|
-
) -> str:
|
|
127
|
+
def generate_coverage_report(requirements: ReqDict, get_status_fn=None) -> str:
|
|
130
128
|
"""Generate text-based coverage report with summary statistics.
|
|
131
129
|
|
|
132
130
|
Args:
|
|
@@ -141,7 +139,9 @@ def generate_coverage_report(
|
|
|
141
139
|
- Breakdown by implementation status (Full/Partial/Unimplemented)
|
|
142
140
|
"""
|
|
143
141
|
if get_status_fn is None:
|
|
144
|
-
|
|
142
|
+
|
|
143
|
+
def get_status_fn(req_id):
|
|
144
|
+
return get_implementation_status(requirements, req_id)
|
|
145
145
|
|
|
146
146
|
lines = []
|
|
147
147
|
lines.append("=== Coverage Report ===")
|
|
@@ -6,7 +6,7 @@ Provides Markdown and CSV generators (no dependencies).
|
|
|
6
6
|
HTML generator is in the html/ subpackage (requires jinja2).
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
from elspais.trace_view.generators.markdown import generate_markdown
|
|
10
9
|
from elspais.trace_view.generators.csv import generate_csv
|
|
10
|
+
from elspais.trace_view.generators.markdown import generate_markdown
|
|
11
11
|
|
|
12
12
|
__all__ = ["generate_markdown", "generate_csv"]
|