elspais 0.11.2__py3-none-any.whl → 0.43.5__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 (147) hide show
  1. elspais/__init__.py +1 -10
  2. elspais/{sponsors/__init__.py → associates.py} +102 -56
  3. elspais/cli.py +366 -69
  4. elspais/commands/__init__.py +9 -3
  5. elspais/commands/analyze.py +118 -169
  6. elspais/commands/changed.py +12 -23
  7. elspais/commands/config_cmd.py +10 -13
  8. elspais/commands/edit.py +33 -13
  9. elspais/commands/example_cmd.py +319 -0
  10. elspais/commands/hash_cmd.py +161 -183
  11. elspais/commands/health.py +1177 -0
  12. elspais/commands/index.py +98 -115
  13. elspais/commands/init.py +99 -22
  14. elspais/commands/reformat_cmd.py +41 -433
  15. elspais/commands/rules_cmd.py +2 -2
  16. elspais/commands/trace.py +443 -324
  17. elspais/commands/validate.py +193 -411
  18. elspais/config/__init__.py +799 -5
  19. elspais/{core/content_rules.py → content_rules.py} +20 -2
  20. elspais/docs/cli/assertions.md +67 -0
  21. elspais/docs/cli/commands.md +304 -0
  22. elspais/docs/cli/config.md +262 -0
  23. elspais/docs/cli/format.md +66 -0
  24. elspais/docs/cli/git.md +45 -0
  25. elspais/docs/cli/health.md +190 -0
  26. elspais/docs/cli/hierarchy.md +60 -0
  27. elspais/docs/cli/ignore.md +72 -0
  28. elspais/docs/cli/mcp.md +245 -0
  29. elspais/docs/cli/quickstart.md +58 -0
  30. elspais/docs/cli/traceability.md +89 -0
  31. elspais/docs/cli/validation.md +96 -0
  32. elspais/graph/GraphNode.py +383 -0
  33. elspais/graph/__init__.py +40 -0
  34. elspais/graph/annotators.py +927 -0
  35. elspais/graph/builder.py +1886 -0
  36. elspais/graph/deserializer.py +248 -0
  37. elspais/graph/factory.py +284 -0
  38. elspais/graph/metrics.py +127 -0
  39. elspais/graph/mutations.py +161 -0
  40. elspais/graph/parsers/__init__.py +156 -0
  41. elspais/graph/parsers/code.py +213 -0
  42. elspais/graph/parsers/comments.py +112 -0
  43. elspais/graph/parsers/config_helpers.py +29 -0
  44. elspais/graph/parsers/heredocs.py +225 -0
  45. elspais/graph/parsers/journey.py +131 -0
  46. elspais/graph/parsers/remainder.py +79 -0
  47. elspais/graph/parsers/requirement.py +347 -0
  48. elspais/graph/parsers/results/__init__.py +6 -0
  49. elspais/graph/parsers/results/junit_xml.py +229 -0
  50. elspais/graph/parsers/results/pytest_json.py +313 -0
  51. elspais/graph/parsers/test.py +305 -0
  52. elspais/graph/relations.py +78 -0
  53. elspais/graph/serialize.py +216 -0
  54. elspais/html/__init__.py +8 -0
  55. elspais/html/generator.py +731 -0
  56. elspais/html/templates/trace_view.html.j2 +2151 -0
  57. elspais/mcp/__init__.py +45 -29
  58. elspais/mcp/__main__.py +5 -1
  59. elspais/mcp/file_mutations.py +138 -0
  60. elspais/mcp/server.py +1998 -244
  61. elspais/testing/__init__.py +3 -3
  62. elspais/testing/config.py +3 -0
  63. elspais/testing/mapper.py +1 -1
  64. elspais/testing/scanner.py +301 -12
  65. elspais/utilities/__init__.py +1 -0
  66. elspais/utilities/docs_loader.py +115 -0
  67. elspais/utilities/git.py +607 -0
  68. elspais/{core → utilities}/hasher.py +8 -22
  69. elspais/utilities/md_renderer.py +189 -0
  70. elspais/{core → utilities}/patterns.py +56 -51
  71. elspais/utilities/reference_config.py +626 -0
  72. elspais/validation/__init__.py +19 -0
  73. elspais/validation/format.py +264 -0
  74. {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/METADATA +7 -4
  75. elspais-0.43.5.dist-info/RECORD +80 -0
  76. elspais/config/defaults.py +0 -179
  77. elspais/config/loader.py +0 -494
  78. elspais/core/__init__.py +0 -21
  79. elspais/core/git.py +0 -346
  80. elspais/core/models.py +0 -320
  81. elspais/core/parser.py +0 -639
  82. elspais/core/rules.py +0 -509
  83. elspais/mcp/context.py +0 -172
  84. elspais/mcp/serializers.py +0 -112
  85. elspais/reformat/__init__.py +0 -50
  86. elspais/reformat/detector.py +0 -112
  87. elspais/reformat/hierarchy.py +0 -247
  88. elspais/reformat/line_breaks.py +0 -218
  89. elspais/reformat/prompts.py +0 -133
  90. elspais/reformat/transformer.py +0 -266
  91. elspais/trace_view/__init__.py +0 -55
  92. elspais/trace_view/coverage.py +0 -183
  93. elspais/trace_view/generators/__init__.py +0 -12
  94. elspais/trace_view/generators/base.py +0 -334
  95. elspais/trace_view/generators/csv.py +0 -118
  96. elspais/trace_view/generators/markdown.py +0 -170
  97. elspais/trace_view/html/__init__.py +0 -33
  98. elspais/trace_view/html/generator.py +0 -1140
  99. elspais/trace_view/html/templates/base.html +0 -283
  100. elspais/trace_view/html/templates/components/code_viewer_modal.html +0 -14
  101. elspais/trace_view/html/templates/components/file_picker_modal.html +0 -20
  102. elspais/trace_view/html/templates/components/legend_modal.html +0 -69
  103. elspais/trace_view/html/templates/components/review_panel.html +0 -118
  104. elspais/trace_view/html/templates/partials/review/help/help-panel.json +0 -244
  105. elspais/trace_view/html/templates/partials/review/help/onboarding.json +0 -77
  106. elspais/trace_view/html/templates/partials/review/help/tooltips.json +0 -237
  107. elspais/trace_view/html/templates/partials/review/review-comments.js +0 -928
  108. elspais/trace_view/html/templates/partials/review/review-data.js +0 -961
  109. elspais/trace_view/html/templates/partials/review/review-help.js +0 -679
  110. elspais/trace_view/html/templates/partials/review/review-init.js +0 -177
  111. elspais/trace_view/html/templates/partials/review/review-line-numbers.js +0 -429
  112. elspais/trace_view/html/templates/partials/review/review-packages.js +0 -1029
  113. elspais/trace_view/html/templates/partials/review/review-position.js +0 -540
  114. elspais/trace_view/html/templates/partials/review/review-resize.js +0 -115
  115. elspais/trace_view/html/templates/partials/review/review-status.js +0 -659
  116. elspais/trace_view/html/templates/partials/review/review-sync.js +0 -992
  117. elspais/trace_view/html/templates/partials/review-styles.css +0 -2238
  118. elspais/trace_view/html/templates/partials/scripts.js +0 -1741
  119. elspais/trace_view/html/templates/partials/styles.css +0 -1756
  120. elspais/trace_view/models.py +0 -378
  121. elspais/trace_view/review/__init__.py +0 -63
  122. elspais/trace_view/review/branches.py +0 -1142
  123. elspais/trace_view/review/models.py +0 -1200
  124. elspais/trace_view/review/position.py +0 -591
  125. elspais/trace_view/review/server.py +0 -1032
  126. elspais/trace_view/review/status.py +0 -455
  127. elspais/trace_view/review/storage.py +0 -1343
  128. elspais/trace_view/scanning.py +0 -213
  129. elspais/trace_view/specs/README.md +0 -84
  130. elspais/trace_view/specs/tv-d00001-template-architecture.md +0 -36
  131. elspais/trace_view/specs/tv-d00002-css-extraction.md +0 -37
  132. elspais/trace_view/specs/tv-d00003-js-extraction.md +0 -43
  133. elspais/trace_view/specs/tv-d00004-build-embedding.md +0 -40
  134. elspais/trace_view/specs/tv-d00005-test-format.md +0 -78
  135. elspais/trace_view/specs/tv-d00010-review-data-models.md +0 -33
  136. elspais/trace_view/specs/tv-d00011-review-storage.md +0 -33
  137. elspais/trace_view/specs/tv-d00012-position-resolution.md +0 -33
  138. elspais/trace_view/specs/tv-d00013-git-branches.md +0 -31
  139. elspais/trace_view/specs/tv-d00014-review-api-server.md +0 -31
  140. elspais/trace_view/specs/tv-d00015-status-modifier.md +0 -27
  141. elspais/trace_view/specs/tv-d00016-js-integration.md +0 -33
  142. elspais/trace_view/specs/tv-p00001-html-generator.md +0 -33
  143. elspais/trace_view/specs/tv-p00002-review-system.md +0 -29
  144. elspais-0.11.2.dist-info/RECORD +0 -101
  145. {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/WHEEL +0 -0
  146. {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/entry_points.txt +0 -0
  147. {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/licenses/LICENSE +0 -0
@@ -1,218 +0,0 @@
1
- # Implements: REQ-int-d00008-C (Line break normalization)
2
- """
3
- Line break normalization for requirement content.
4
-
5
- Provides functions to:
6
- - Remove unnecessary blank lines after section headers
7
- - Reflow paragraphs (join lines broken mid-sentence)
8
- - Preserve intentional structure (list items, code blocks)
9
-
10
- IMPLEMENTS REQUIREMENTS:
11
- REQ-int-d00008-C: Line break normalization SHALL be included.
12
- """
13
-
14
- import re
15
- from typing import List, Tuple
16
-
17
-
18
- def normalize_line_breaks(content: str, reflow: bool = True) -> str:
19
- """
20
- Normalize line breaks in requirement content.
21
-
22
- Args:
23
- content: Raw requirement markdown content
24
- reflow: If True, also reflow paragraphs (join broken lines)
25
-
26
- Returns:
27
- Content with normalized line breaks
28
- """
29
- lines = content.split("\n")
30
- result_lines: List[str] = []
31
-
32
- i = 0
33
- while i < len(lines):
34
- line = lines[i]
35
-
36
- # Check if this is a section header (## Something)
37
- if re.match(r"^##\s+\w", line):
38
- result_lines.append(line)
39
- # Skip blank lines immediately after section header
40
- i += 1
41
- while i < len(lines) and lines[i].strip() == "":
42
- i += 1
43
- # Add single blank line after header for readability
44
- result_lines.append("")
45
- continue
46
-
47
- # Check if this starts a paragraph that might need reflowing
48
- if reflow and line.strip() and not _is_structural_line(line):
49
- # Collect paragraph lines
50
- para_lines = [line.rstrip()]
51
- i += 1
52
- while i < len(lines):
53
- next_line = lines[i]
54
- # Stop at blank lines, structural elements, or next section
55
- if (
56
- next_line.strip() == ""
57
- or _is_structural_line(next_line)
58
- or re.match(r"^##\s+", next_line)
59
- ):
60
- break
61
- para_lines.append(next_line.rstrip())
62
- i += 1
63
-
64
- # Join and reflow the paragraph
65
- reflowed = _reflow_paragraph(para_lines)
66
- result_lines.append(reflowed)
67
- continue
68
-
69
- # Keep structural lines and blank lines as-is
70
- result_lines.append(line.rstrip())
71
- i += 1
72
-
73
- # Clean up multiple consecutive blank lines
74
- return _collapse_blank_lines("\n".join(result_lines))
75
-
76
-
77
- def _is_structural_line(line: str) -> bool:
78
- """
79
- Check if a line is structural (should not be reflowed).
80
-
81
- Structural lines include:
82
- - List items (A., B., 1., -, *)
83
- - Headers (# or ##)
84
- - Metadata lines (**Level**: etc)
85
- - End markers (*End* ...)
86
- - Code fence markers (```)
87
- """
88
- stripped = line.strip()
89
-
90
- if not stripped:
91
- return False
92
-
93
- # Headers
94
- if stripped.startswith("#"):
95
- return True
96
-
97
- # Lettered assertions (A. B. C. etc)
98
- if re.match(r"^[A-Z]\.\s", stripped):
99
- return True
100
-
101
- # Numbered lists (1. 2. 3. etc)
102
- if re.match(r"^\d+\.\s", stripped):
103
- return True
104
-
105
- # Bullet points
106
- if stripped.startswith(("- ", "* ", "+ ")):
107
- return True
108
-
109
- # Metadata line
110
- if stripped.startswith("**Level**:") or stripped.startswith("**Status**:"):
111
- return True
112
-
113
- # Combined metadata line
114
- if re.match(r"\*\*Level\*\*:", stripped):
115
- return True
116
-
117
- # End marker
118
- if stripped.startswith("*End*"):
119
- return True
120
-
121
- # Code fence
122
- if stripped.startswith("```"):
123
- return True
124
-
125
- return False
126
-
127
-
128
- def _reflow_paragraph(lines: List[str]) -> str:
129
- """
130
- Reflow a list of paragraph lines into a single line.
131
-
132
- Args:
133
- lines: Lines that form a paragraph
134
-
135
- Returns:
136
- Single reflowed line
137
- """
138
- if not lines:
139
- return ""
140
-
141
- if len(lines) == 1:
142
- return lines[0]
143
-
144
- # Join lines with space, collapsing multiple spaces
145
- joined = " ".join(line.strip() for line in lines if line.strip())
146
- # Collapse multiple spaces
147
- return re.sub(r"\s+", " ", joined)
148
-
149
-
150
- def _collapse_blank_lines(content: str) -> str:
151
- """
152
- Collapse multiple consecutive blank lines into single blank lines.
153
-
154
- Args:
155
- content: Content that may have multiple blank lines
156
-
157
- Returns:
158
- Content with at most one blank line between paragraphs
159
- """
160
- # Replace 3+ newlines with 2 newlines (one blank line)
161
- return re.sub(r"\n{3,}", "\n\n", content)
162
-
163
-
164
- def fix_requirement_line_breaks(body: str, rationale: str, reflow: bool = True) -> Tuple[str, str]:
165
- """
166
- Fix line breaks in requirement body and rationale.
167
-
168
- Args:
169
- body: Requirement body text
170
- rationale: Requirement rationale text
171
- reflow: Whether to reflow paragraphs
172
-
173
- Returns:
174
- Tuple of (fixed_body, fixed_rationale)
175
- """
176
- fixed_body = normalize_line_breaks(body, reflow=reflow) if body else ""
177
- fixed_rationale = normalize_line_breaks(rationale, reflow=reflow) if rationale else ""
178
-
179
- return fixed_body, fixed_rationale
180
-
181
-
182
- def detect_line_break_issues(content: str) -> List[str]:
183
- """
184
- Detect potential line break issues in content.
185
-
186
- Returns list of issues found for reporting.
187
- """
188
- issues = []
189
- lines = content.split("\n")
190
-
191
- for i, line in enumerate(lines):
192
- # Check for blank line after section header
193
- if re.match(r"^##\s+\w", line):
194
- # Look ahead for multiple blank lines
195
- blank_count = 0
196
- j = i + 1
197
- while j < len(lines) and lines[j].strip() == "":
198
- blank_count += 1
199
- j += 1
200
- if blank_count > 1:
201
- issues.append(
202
- f"Line {i+1}: Multiple blank lines ({blank_count}) after section header"
203
- )
204
-
205
- # Check for mid-sentence line break (line ends without punctuation)
206
- stripped = line.rstrip()
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
- # Line ends with a word (not punctuation), followed by non-empty line
215
- if stripped and stripped[-1].isalnum():
216
- issues.append(f"Line {i+1}: Possible mid-sentence line break")
217
-
218
- return issues
@@ -1,133 +0,0 @@
1
- # Implements: REQ-int-d00008 (Reformat Command)
2
- """
3
- Prompts and JSON schema for Claude Code CLI integration.
4
-
5
- Defines the system prompt and output schema for AI-assisted
6
- requirement reformatting.
7
- """
8
-
9
- import json
10
-
11
- # JSON Schema for structured output validation
12
- JSON_SCHEMA = {
13
- "type": "object",
14
- "properties": {
15
- "rationale": {
16
- "type": "string",
17
- "description": (
18
- "Non-normative context explaining why this requirement exists. "
19
- "No SHALL/MUST language."
20
- ),
21
- },
22
- "assertions": {
23
- "type": "array",
24
- "items": {"type": "string"},
25
- "description": (
26
- "List of assertions, each starting with 'The system SHALL...' "
27
- "or similar prescriptive language."
28
- ),
29
- },
30
- },
31
- "required": ["rationale", "assertions"],
32
- }
33
-
34
- JSON_SCHEMA_STR = json.dumps(JSON_SCHEMA, separators=(",", ":"))
35
-
36
- # System prompt for requirement reformatting
37
- REFORMAT_SYSTEM_PROMPT = """You are a requirements engineering expert specializing in \
38
- FDA 21 CFR Part 11 compliant clinical trial systems.
39
-
40
- Your task is to reformat requirements from an old descriptive format to a new \
41
- prescriptive assertion-based format.
42
-
43
- EXTRACTION RULES:
44
- 1. Extract ALL obligations from the old format (body text, bullet points, acceptance criteria)
45
- 2. Convert each distinct obligation to a labeled assertion
46
- 3. Each assertion MUST use "SHALL" for mandatory obligations or "SHALL NOT" for prohibitions
47
- 4. Each assertion MUST be independently testable (decidable as true/false)
48
- 5. Assertions MUST be prescriptive, not descriptive
49
- 6. Maximum 26 assertions (A-Z) - if more needed, consolidate related obligations
50
- 7. Do NOT add obligations that were not in the original
51
- 8. Do NOT remove or weaken any obligations from the original
52
- 9. Combine related acceptance criteria into single assertions when appropriate
53
-
54
- RATIONALE RULES:
55
- 1. The rationale provides context for WHY this requirement exists
56
- 2. Rationale MUST NOT introduce new obligations
57
- 3. Rationale MUST NOT use SHALL/MUST language
58
- 4. Rationale can explain regulatory context, design decisions, or relationships
59
-
60
- LANGUAGE GUIDELINES:
61
- - Use "The system SHALL..." for system behaviors
62
- - Use "The platform SHALL..." for platform-wide requirements
63
- - Use "Data SHALL..." for data-related requirements
64
- - Be specific and unambiguous
65
- - Avoid vague terms like "appropriate", "adequate", "reasonable" unless quantified
66
-
67
- OUTPUT FORMAT:
68
- Return a JSON object with:
69
- - "rationale": A paragraph explaining the requirement's purpose (no SHALL language)
70
- - "assertions": An array of strings, each a complete assertion with subject and SHALL
71
-
72
- Example output:
73
- {
74
- "rationale": "This requirement ensures complete audit trails for regulatory compliance. \
75
- FDA 21 CFR Part 11 mandates tamper-evident histories of all modifications.",
76
- "assertions": [
77
- "The system SHALL store all data changes as immutable events.",
78
- "The system SHALL preserve the complete history of all modifications.",
79
- "Event records SHALL include timestamp, user ID, and action type.",
80
- "The system SHALL NOT allow modification or deletion of stored events."
81
- ]
82
- }"""
83
-
84
-
85
- def build_user_prompt(
86
- req_id: str,
87
- title: str,
88
- level: str,
89
- status: str,
90
- implements: list,
91
- body: str,
92
- rationale: str = "",
93
- ) -> str:
94
- """
95
- Build the user prompt for reformatting a requirement.
96
-
97
- Args:
98
- req_id: Requirement ID (e.g., 'REQ-p00046')
99
- title: Requirement title
100
- level: Requirement level (PRD, Dev, Ops)
101
- status: Requirement status (Draft, Active, etc.)
102
- implements: List of parent requirement IDs
103
- body: Current requirement body text
104
- rationale: Current rationale text (if any)
105
-
106
- Returns:
107
- User prompt string
108
- """
109
- implements_str = ", ".join(implements) if implements else "-"
110
-
111
- prompt = f"""Reformat the following requirement from old format to new assertion-based format.
112
-
113
- REQUIREMENT ID: {req_id}
114
- TITLE: {title}
115
- LEVEL: {level}
116
- STATUS: {status}
117
- IMPLEMENTS: {implements_str}
118
-
119
- CURRENT BODY:
120
- {body}
121
- """
122
-
123
- if rationale and rationale.strip():
124
- prompt += f"""
125
- CURRENT RATIONALE:
126
- {rationale}
127
- """
128
-
129
- prompt += """
130
- Extract all obligations and convert them to labeled assertions. \
131
- Return ONLY the JSON object with "rationale" and "assertions" fields."""
132
-
133
- return prompt
@@ -1,266 +0,0 @@
1
- # Implements: REQ-int-d00008 (Reformat Command)
2
- """
3
- AI-assisted requirement transformation using Claude Code CLI.
4
-
5
- Invokes claude CLI to reformat requirements and assembles the output
6
- into the new format.
7
- """
8
-
9
- import json
10
- import subprocess
11
- import sys
12
- from typing import List, Optional, Tuple
13
-
14
- from elspais.reformat.hierarchy import RequirementNode
15
- from elspais.reformat.prompts import JSON_SCHEMA_STR, REFORMAT_SYSTEM_PROMPT, build_user_prompt
16
-
17
-
18
- def reformat_requirement(
19
- node: RequirementNode, model: str = "sonnet", verbose: bool = False
20
- ) -> Tuple[Optional[dict], bool, str]:
21
- """
22
- Use Claude CLI to reformat a requirement.
23
-
24
- Args:
25
- node: RequirementNode with current content
26
- model: Claude model to use (sonnet, opus, haiku)
27
- verbose: Print debug information
28
-
29
- Returns:
30
- Tuple of (parsed_result, success, error_message)
31
- parsed_result is a dict with 'rationale' and 'assertions' keys
32
- """
33
- # Build the prompt
34
- user_prompt = build_user_prompt(
35
- req_id=node.req_id,
36
- title=node.title,
37
- level=node.level,
38
- status=node.status,
39
- implements=node.implements,
40
- body=node.body,
41
- rationale=node.rationale,
42
- )
43
-
44
- # Build the claude command
45
- cmd = [
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,
59
- ]
60
-
61
- if verbose:
62
- print(" Running: claude -p --output-format json ...", file=sys.stderr)
63
-
64
- try:
65
- result = subprocess.run(
66
- cmd, capture_output=True, text=True, timeout=120 # 2 minute timeout
67
- )
68
-
69
- if result.returncode != 0:
70
- error_msg = result.stderr.strip() if result.stderr else "Unknown error"
71
- return None, False, f"Claude CLI failed: {error_msg}"
72
-
73
- # Parse the JSON response
74
- parsed = parse_claude_response(result.stdout)
75
- if parsed is None:
76
- return None, False, "Failed to parse Claude response"
77
-
78
- return parsed, True, ""
79
-
80
- except subprocess.TimeoutExpired:
81
- return None, False, "Claude CLI timed out"
82
- except FileNotFoundError:
83
- return None, False, "Claude CLI not found - ensure 'claude' is in PATH"
84
- except Exception as e:
85
- return None, False, f"Unexpected error: {e}"
86
-
87
-
88
- def parse_claude_response(response: str) -> Optional[dict]:
89
- """
90
- Parse the JSON response from Claude CLI.
91
-
92
- The response format with --output-format json is a JSON object containing:
93
- - type: "result"
94
- - subtype: "success" or "error"
95
- - structured_output: the actual JSON matching our schema
96
- - result: text result (may be empty with structured output)
97
-
98
- Args:
99
- response: Raw stdout from claude CLI
100
-
101
- Returns:
102
- Parsed dict with 'rationale' and 'assertions', or None on failure
103
- """
104
- try:
105
- data = json.loads(response)
106
-
107
- # Check for error
108
- if data.get("is_error") or data.get("subtype") == "error":
109
- return None
110
-
111
- # The structured output is in 'structured_output' field
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
- ):
119
- return structured
120
-
121
- # Fallback: Direct result (if schema not used)
122
- if "rationale" in data and "assertions" in data:
123
- return data
124
-
125
- # Fallback: Wrapped in result field
126
- if "result" in data:
127
- result = data["result"]
128
- if isinstance(result, dict) and "rationale" in result:
129
- return result
130
- # Result might be a JSON string
131
- if isinstance(result, str) and result.strip():
132
- try:
133
- parsed = json.loads(result)
134
- if "rationale" in parsed:
135
- return parsed
136
- except json.JSONDecodeError:
137
- pass
138
-
139
- return None
140
-
141
- except json.JSONDecodeError:
142
- # Try to extract JSON from the response
143
- try:
144
- json_start = response.find("{")
145
- json_end = response.rfind("}") + 1
146
- if json_start >= 0 and json_end > json_start:
147
- parsed = json.loads(response[json_start:json_end])
148
- if "structured_output" in parsed:
149
- return parsed["structured_output"]
150
- if "rationale" in parsed and "assertions" in parsed:
151
- return parsed
152
- except json.JSONDecodeError:
153
- pass
154
- return None
155
-
156
-
157
- def assemble_new_format(
158
- req_id: str,
159
- title: str,
160
- level: str,
161
- status: str,
162
- implements: List[str],
163
- rationale: str,
164
- assertions: List[str],
165
- ) -> str:
166
- """
167
- Assemble the new format requirement markdown.
168
-
169
- Args:
170
- req_id: Requirement ID (e.g., 'REQ-p00046')
171
- title: Requirement title
172
- level: Requirement level (PRD, Dev, Ops)
173
- status: Requirement status
174
- implements: List of parent requirement IDs
175
- rationale: Rationale text (from AI)
176
- assertions: List of assertion strings (from AI)
177
-
178
- Returns:
179
- Complete requirement markdown in new format
180
- """
181
- # Format implements field
182
- if implements:
183
- implements_str = ", ".join(implements)
184
- else:
185
- implements_str = "-"
186
-
187
- # Build header
188
- lines = [
189
- f"# {req_id}: {title}",
190
- "",
191
- f"**Level**: {level} | **Status**: {status} | **Implements**: {implements_str}",
192
- "",
193
- ]
194
-
195
- # Add rationale section
196
- lines.append("## Rationale")
197
- lines.append("")
198
- lines.append(rationale.strip())
199
- lines.append("")
200
-
201
- # Add assertions section
202
- lines.append("## Assertions")
203
- lines.append("")
204
-
205
- # Label assertions A, B, C, etc.
206
- for i, assertion in enumerate(assertions):
207
- label = chr(ord("A") + i)
208
- # Clean up assertion text
209
- assertion_text = assertion.strip()
210
- # Remove any existing label if present
211
- if len(assertion_text) > 2 and assertion_text[1] == "." and assertion_text[0].isupper():
212
- assertion_text = assertion_text[2:].strip()
213
- lines.append(f"{label}. {assertion_text}")
214
-
215
- lines.append("")
216
-
217
- # Add footer with placeholder hash (will be updated by elspais)
218
- # Use 8 zeros as placeholder - elspais expects valid hex format
219
- lines.append(f"*End* *{title}* | **Hash**: 00000000")
220
- lines.append("")
221
-
222
- return "\n".join(lines)
223
-
224
-
225
- def validate_reformatted_content(
226
- original: RequirementNode, rationale: str, assertions: List[str]
227
- ) -> Tuple[bool, List[str]]:
228
- """
229
- Validate that reformatted content is well-formed.
230
-
231
- Args:
232
- original: Original requirement node
233
- rationale: New rationale text
234
- assertions: New assertions list
235
-
236
- Returns:
237
- Tuple of (is_valid, list of warnings)
238
- """
239
- warnings = []
240
-
241
- # Check assertions exist
242
- if not assertions:
243
- warnings.append("No assertions generated")
244
- return False, warnings
245
-
246
- # Check each assertion uses SHALL
247
- for i, assertion in enumerate(assertions):
248
- label = chr(ord("A") + i)
249
- if "SHALL" not in assertion.upper():
250
- warnings.append(f"Assertion {label} missing SHALL keyword")
251
-
252
- # Check rationale doesn't use SHALL
253
- if "SHALL" in rationale.upper():
254
- warnings.append("Rationale contains SHALL (should be non-normative)")
255
-
256
- # Check assertion count
257
- if len(assertions) > 26:
258
- warnings.append(f"Too many assertions ({len(assertions)} > 26)")
259
- return False, warnings
260
-
261
- # Warning if very few assertions from complex body
262
- if len(assertions) < 2 and len(original.body) > 500:
263
- warnings.append("Few assertions from large body - may have missed obligations")
264
-
265
- is_valid = not any("missing SHALL" in w or "No assertions" in w for w in warnings)
266
- return is_valid, warnings
@@ -1,55 +0,0 @@
1
- # Implements: REQ-int-d00001-A (trace_view package at src/elspais/trace_view/)
2
- """
3
- elspais.trace_view - Interactive traceability matrix generation.
4
-
5
- This package provides enhanced traceability features including:
6
- - Interactive HTML generation with collapsible hierarchies
7
- - Implementation file scanning
8
- - Git state tracking (uncommitted, modified, moved files)
9
- - Review system with comment threads and approval workflows
10
-
11
- Optional dependencies:
12
- - pip install elspais[trace-view] for HTML generation (requires jinja2)
13
- - pip install elspais[trace-review] for review server (requires flask)
14
- """
15
-
16
- from elspais.trace_view.generators.base import TraceViewGenerator
17
- from elspais.trace_view.models import GitChangeInfo, TestInfo, TraceViewRequirement
18
-
19
- __all__ = [
20
- "TraceViewRequirement",
21
- "TestInfo",
22
- "GitChangeInfo",
23
- "TraceViewGenerator",
24
- "generate_markdown",
25
- "generate_csv",
26
- "generate_html",
27
- ]
28
-
29
-
30
- def generate_markdown(requirements, **kwargs):
31
- """Generate Markdown traceability matrix."""
32
- from elspais.trace_view.generators.markdown import generate_markdown as _gen
33
-
34
- return _gen(requirements, **kwargs)
35
-
36
-
37
- def generate_csv(requirements, **kwargs):
38
- """Generate CSV traceability matrix."""
39
- from elspais.trace_view.generators.csv import generate_csv as _gen
40
-
41
- return _gen(requirements, **kwargs)
42
-
43
-
44
- def generate_html(requirements, **kwargs):
45
- """Generate interactive HTML traceability matrix.
46
-
47
- Requires jinja2: pip install elspais[trace-view]
48
- """
49
- try:
50
- from elspais.trace_view.html import HTMLGenerator
51
- except ImportError as e:
52
- raise ImportError(
53
- "HTML generation requires Jinja2. " "Install with: pip install elspais[trace-view]"
54
- ) from e
55
- return HTMLGenerator(requirements, **kwargs).generate()