elspais 0.9.1__py3-none-any.whl → 0.11.0__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 (75) hide show
  1. elspais/cli.py +123 -1
  2. elspais/commands/changed.py +160 -0
  3. elspais/commands/hash_cmd.py +72 -26
  4. elspais/commands/reformat_cmd.py +458 -0
  5. elspais/commands/trace.py +157 -3
  6. elspais/commands/validate.py +81 -18
  7. elspais/core/git.py +352 -0
  8. elspais/core/models.py +2 -0
  9. elspais/core/parser.py +68 -24
  10. elspais/reformat/__init__.py +50 -0
  11. elspais/reformat/detector.py +119 -0
  12. elspais/reformat/hierarchy.py +246 -0
  13. elspais/reformat/line_breaks.py +220 -0
  14. elspais/reformat/prompts.py +123 -0
  15. elspais/reformat/transformer.py +264 -0
  16. elspais/sponsors/__init__.py +432 -0
  17. elspais/trace_view/__init__.py +54 -0
  18. elspais/trace_view/coverage.py +183 -0
  19. elspais/trace_view/generators/__init__.py +12 -0
  20. elspais/trace_view/generators/base.py +329 -0
  21. elspais/trace_view/generators/csv.py +122 -0
  22. elspais/trace_view/generators/markdown.py +175 -0
  23. elspais/trace_view/html/__init__.py +31 -0
  24. elspais/trace_view/html/generator.py +1006 -0
  25. elspais/trace_view/html/templates/base.html +283 -0
  26. elspais/trace_view/html/templates/components/code_viewer_modal.html +14 -0
  27. elspais/trace_view/html/templates/components/file_picker_modal.html +20 -0
  28. elspais/trace_view/html/templates/components/legend_modal.html +69 -0
  29. elspais/trace_view/html/templates/components/review_panel.html +118 -0
  30. elspais/trace_view/html/templates/partials/review/help/help-panel.json +244 -0
  31. elspais/trace_view/html/templates/partials/review/help/onboarding.json +77 -0
  32. elspais/trace_view/html/templates/partials/review/help/tooltips.json +237 -0
  33. elspais/trace_view/html/templates/partials/review/review-comments.js +928 -0
  34. elspais/trace_view/html/templates/partials/review/review-data.js +961 -0
  35. elspais/trace_view/html/templates/partials/review/review-help.js +679 -0
  36. elspais/trace_view/html/templates/partials/review/review-init.js +177 -0
  37. elspais/trace_view/html/templates/partials/review/review-line-numbers.js +429 -0
  38. elspais/trace_view/html/templates/partials/review/review-packages.js +1029 -0
  39. elspais/trace_view/html/templates/partials/review/review-position.js +540 -0
  40. elspais/trace_view/html/templates/partials/review/review-resize.js +115 -0
  41. elspais/trace_view/html/templates/partials/review/review-status.js +659 -0
  42. elspais/trace_view/html/templates/partials/review/review-sync.js +992 -0
  43. elspais/trace_view/html/templates/partials/review-styles.css +2238 -0
  44. elspais/trace_view/html/templates/partials/scripts.js +1741 -0
  45. elspais/trace_view/html/templates/partials/styles.css +1756 -0
  46. elspais/trace_view/models.py +353 -0
  47. elspais/trace_view/review/__init__.py +60 -0
  48. elspais/trace_view/review/branches.py +1149 -0
  49. elspais/trace_view/review/models.py +1205 -0
  50. elspais/trace_view/review/position.py +609 -0
  51. elspais/trace_view/review/server.py +1056 -0
  52. elspais/trace_view/review/status.py +470 -0
  53. elspais/trace_view/review/storage.py +1367 -0
  54. elspais/trace_view/scanning.py +213 -0
  55. elspais/trace_view/specs/README.md +84 -0
  56. elspais/trace_view/specs/tv-d00001-template-architecture.md +36 -0
  57. elspais/trace_view/specs/tv-d00002-css-extraction.md +37 -0
  58. elspais/trace_view/specs/tv-d00003-js-extraction.md +43 -0
  59. elspais/trace_view/specs/tv-d00004-build-embedding.md +40 -0
  60. elspais/trace_view/specs/tv-d00005-test-format.md +78 -0
  61. elspais/trace_view/specs/tv-d00010-review-data-models.md +33 -0
  62. elspais/trace_view/specs/tv-d00011-review-storage.md +33 -0
  63. elspais/trace_view/specs/tv-d00012-position-resolution.md +33 -0
  64. elspais/trace_view/specs/tv-d00013-git-branches.md +31 -0
  65. elspais/trace_view/specs/tv-d00014-review-api-server.md +31 -0
  66. elspais/trace_view/specs/tv-d00015-status-modifier.md +27 -0
  67. elspais/trace_view/specs/tv-d00016-js-integration.md +33 -0
  68. elspais/trace_view/specs/tv-p00001-html-generator.md +33 -0
  69. elspais/trace_view/specs/tv-p00002-review-system.md +29 -0
  70. {elspais-0.9.1.dist-info → elspais-0.11.0.dist-info}/METADATA +78 -26
  71. elspais-0.11.0.dist-info/RECORD +101 -0
  72. elspais-0.9.1.dist-info/RECORD +0 -38
  73. {elspais-0.9.1.dist-info → elspais-0.11.0.dist-info}/WHEEL +0 -0
  74. {elspais-0.9.1.dist-info → elspais-0.11.0.dist-info}/entry_points.txt +0 -0
  75. {elspais-0.9.1.dist-info → elspais-0.11.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,123 @@
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": "Non-normative context explaining why this requirement exists. No SHALL/MUST language."
18
+ },
19
+ "assertions": {
20
+ "type": "array",
21
+ "items": {"type": "string"},
22
+ "description": "List of assertions, each starting with 'The system SHALL...' or similar prescriptive language."
23
+ }
24
+ },
25
+ "required": ["rationale", "assertions"]
26
+ }
27
+
28
+ JSON_SCHEMA_STR = json.dumps(JSON_SCHEMA, separators=(',', ':'))
29
+
30
+ # System prompt for requirement reformatting
31
+ REFORMAT_SYSTEM_PROMPT = """You are a requirements engineering expert specializing in FDA 21 CFR Part 11 compliant clinical trial systems.
32
+
33
+ Your task is to reformat requirements from an old descriptive format to a new prescriptive assertion-based format.
34
+
35
+ EXTRACTION RULES:
36
+ 1. Extract ALL obligations from the old format (body text, bullet points, acceptance criteria)
37
+ 2. Convert each distinct obligation to a labeled assertion
38
+ 3. Each assertion MUST use "SHALL" for mandatory obligations or "SHALL NOT" for prohibitions
39
+ 4. Each assertion MUST be independently testable (decidable as true/false)
40
+ 5. Assertions MUST be prescriptive, not descriptive
41
+ 6. Maximum 26 assertions (A-Z) - if more needed, consolidate related obligations
42
+ 7. Do NOT add obligations that were not in the original
43
+ 8. Do NOT remove or weaken any obligations from the original
44
+ 9. Combine related acceptance criteria into single assertions when appropriate
45
+
46
+ RATIONALE RULES:
47
+ 1. The rationale provides context for WHY this requirement exists
48
+ 2. Rationale MUST NOT introduce new obligations
49
+ 3. Rationale MUST NOT use SHALL/MUST language
50
+ 4. Rationale can explain regulatory context, design decisions, or relationships to other requirements
51
+
52
+ LANGUAGE GUIDELINES:
53
+ - Use "The system SHALL..." for system behaviors
54
+ - Use "The platform SHALL..." for platform-wide requirements
55
+ - Use "Data SHALL..." for data-related requirements
56
+ - Be specific and unambiguous
57
+ - Avoid vague terms like "appropriate", "adequate", "reasonable" unless quantified
58
+
59
+ OUTPUT FORMAT:
60
+ Return a JSON object with:
61
+ - "rationale": A paragraph explaining the requirement's purpose (no SHALL language)
62
+ - "assertions": An array of strings, each being a complete assertion starting with the subject and SHALL
63
+
64
+ Example output:
65
+ {
66
+ "rationale": "This requirement ensures complete audit trails for regulatory compliance. FDA 21 CFR Part 11 mandates that electronic records maintain tamper-evident histories of all modifications.",
67
+ "assertions": [
68
+ "The system SHALL store all data changes as immutable events.",
69
+ "The system SHALL preserve the complete history of all modifications.",
70
+ "Event records SHALL include timestamp, user ID, and action type.",
71
+ "The system SHALL NOT allow modification or deletion of stored events."
72
+ ]
73
+ }"""
74
+
75
+
76
+ def build_user_prompt(
77
+ req_id: str,
78
+ title: str,
79
+ level: str,
80
+ status: str,
81
+ implements: list,
82
+ body: str,
83
+ rationale: str = ""
84
+ ) -> str:
85
+ """
86
+ Build the user prompt for reformatting a requirement.
87
+
88
+ Args:
89
+ req_id: Requirement ID (e.g., 'REQ-p00046')
90
+ title: Requirement title
91
+ level: Requirement level (PRD, Dev, Ops)
92
+ status: Requirement status (Draft, Active, etc.)
93
+ implements: List of parent requirement IDs
94
+ body: Current requirement body text
95
+ rationale: Current rationale text (if any)
96
+
97
+ Returns:
98
+ User prompt string
99
+ """
100
+ implements_str = ", ".join(implements) if implements else "-"
101
+
102
+ prompt = f"""Reformat the following requirement from old format to new assertion-based format.
103
+
104
+ REQUIREMENT ID: {req_id}
105
+ TITLE: {title}
106
+ LEVEL: {level}
107
+ STATUS: {status}
108
+ IMPLEMENTS: {implements_str}
109
+
110
+ CURRENT BODY:
111
+ {body}
112
+ """
113
+
114
+ if rationale and rationale.strip():
115
+ prompt += f"""
116
+ CURRENT RATIONALE:
117
+ {rationale}
118
+ """
119
+
120
+ prompt += """
121
+ Extract all obligations and convert them to labeled assertions. Return ONLY the JSON object with "rationale" and "assertions" fields."""
122
+
123
+ return prompt
@@ -0,0 +1,264 @@
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 REFORMAT_SYSTEM_PROMPT, JSON_SCHEMA_STR, build_user_prompt
16
+
17
+
18
+ def reformat_requirement(
19
+ node: RequirementNode,
20
+ model: str = "sonnet",
21
+ verbose: bool = False
22
+ ) -> Tuple[Optional[dict], bool, str]:
23
+ """
24
+ Use Claude CLI to reformat a requirement.
25
+
26
+ Args:
27
+ node: RequirementNode with current content
28
+ model: Claude model to use (sonnet, opus, haiku)
29
+ verbose: Print debug information
30
+
31
+ Returns:
32
+ Tuple of (parsed_result, success, error_message)
33
+ parsed_result is a dict with 'rationale' and 'assertions' keys
34
+ """
35
+ # Build the prompt
36
+ user_prompt = build_user_prompt(
37
+ req_id=node.req_id,
38
+ title=node.title,
39
+ level=node.level,
40
+ status=node.status,
41
+ implements=node.implements,
42
+ body=node.body,
43
+ rationale=node.rationale
44
+ )
45
+
46
+ # Build the claude command
47
+ cmd = [
48
+ 'claude',
49
+ '-p', # Print mode (non-interactive)
50
+ '--output-format', 'json',
51
+ '--json-schema', JSON_SCHEMA_STR,
52
+ '--system-prompt', REFORMAT_SYSTEM_PROMPT,
53
+ '--tools', '', # Disable all tools
54
+ '--model', model,
55
+ user_prompt
56
+ ]
57
+
58
+ if verbose:
59
+ print(f" Running: claude -p --output-format json ...", file=sys.stderr)
60
+
61
+ try:
62
+ result = subprocess.run(
63
+ cmd,
64
+ capture_output=True,
65
+ text=True,
66
+ 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 isinstance(structured, dict) and 'rationale' in structured and 'assertions' in structured:
115
+ return structured
116
+
117
+ # Fallback: Direct result (if schema not used)
118
+ if 'rationale' in data and 'assertions' in data:
119
+ return data
120
+
121
+ # Fallback: Wrapped in result field
122
+ if 'result' in data:
123
+ result = data['result']
124
+ if isinstance(result, dict) and 'rationale' in result:
125
+ return result
126
+ # Result might be a JSON string
127
+ if isinstance(result, str) and result.strip():
128
+ try:
129
+ parsed = json.loads(result)
130
+ if 'rationale' in parsed:
131
+ return parsed
132
+ except json.JSONDecodeError:
133
+ pass
134
+
135
+ return None
136
+
137
+ except json.JSONDecodeError:
138
+ # Try to extract JSON from the response
139
+ try:
140
+ json_start = response.find('{')
141
+ json_end = response.rfind('}') + 1
142
+ if json_start >= 0 and json_end > json_start:
143
+ parsed = json.loads(response[json_start:json_end])
144
+ if 'structured_output' in parsed:
145
+ return parsed['structured_output']
146
+ if 'rationale' in parsed and 'assertions' in parsed:
147
+ return parsed
148
+ except json.JSONDecodeError:
149
+ pass
150
+ return None
151
+
152
+
153
+ def assemble_new_format(
154
+ req_id: str,
155
+ title: str,
156
+ level: str,
157
+ status: str,
158
+ implements: List[str],
159
+ rationale: str,
160
+ assertions: List[str]
161
+ ) -> str:
162
+ """
163
+ Assemble the new format requirement markdown.
164
+
165
+ Args:
166
+ req_id: Requirement ID (e.g., 'REQ-p00046')
167
+ title: Requirement title
168
+ level: Requirement level (PRD, Dev, Ops)
169
+ status: Requirement status
170
+ implements: List of parent requirement IDs
171
+ rationale: Rationale text (from AI)
172
+ assertions: List of assertion strings (from AI)
173
+
174
+ Returns:
175
+ Complete requirement markdown in new format
176
+ """
177
+ # Format implements field
178
+ if implements:
179
+ implements_str = ", ".join(implements)
180
+ else:
181
+ implements_str = "-"
182
+
183
+ # Build header
184
+ lines = [
185
+ f"# {req_id}: {title}",
186
+ "",
187
+ f"**Level**: {level} | **Status**: {status} | **Implements**: {implements_str}",
188
+ "",
189
+ ]
190
+
191
+ # Add rationale section
192
+ lines.append("## Rationale")
193
+ lines.append("")
194
+ lines.append(rationale.strip())
195
+ lines.append("")
196
+
197
+ # Add assertions section
198
+ lines.append("## Assertions")
199
+ lines.append("")
200
+
201
+ # Label assertions A, B, C, etc.
202
+ for i, assertion in enumerate(assertions):
203
+ label = chr(ord('A') + i)
204
+ # Clean up assertion text
205
+ assertion_text = assertion.strip()
206
+ # Remove any existing label if present
207
+ if len(assertion_text) > 2 and assertion_text[1] == '.' and assertion_text[0].isupper():
208
+ assertion_text = assertion_text[2:].strip()
209
+ lines.append(f"{label}. {assertion_text}")
210
+
211
+ lines.append("")
212
+
213
+ # Add footer with placeholder hash (will be updated by elspais)
214
+ # Use 8 zeros as placeholder - elspais expects valid hex format
215
+ lines.append(f"*End* *{title}* | **Hash**: 00000000")
216
+ lines.append("")
217
+
218
+ return "\n".join(lines)
219
+
220
+
221
+ def validate_reformatted_content(
222
+ original: RequirementNode,
223
+ rationale: str,
224
+ assertions: List[str]
225
+ ) -> Tuple[bool, List[str]]:
226
+ """
227
+ Validate that reformatted content is well-formed.
228
+
229
+ Args:
230
+ original: Original requirement node
231
+ rationale: New rationale text
232
+ assertions: New assertions list
233
+
234
+ Returns:
235
+ Tuple of (is_valid, list of warnings)
236
+ """
237
+ warnings = []
238
+
239
+ # Check assertions exist
240
+ if not assertions:
241
+ warnings.append("No assertions generated")
242
+ return False, warnings
243
+
244
+ # Check each assertion uses SHALL
245
+ for i, assertion in enumerate(assertions):
246
+ label = chr(ord('A') + i)
247
+ if 'SHALL' not in assertion.upper():
248
+ warnings.append(f"Assertion {label} missing SHALL keyword")
249
+
250
+ # Check rationale doesn't use SHALL
251
+ if 'SHALL' in rationale.upper():
252
+ warnings.append("Rationale contains SHALL (should be non-normative)")
253
+
254
+ # Check assertion count
255
+ if len(assertions) > 26:
256
+ warnings.append(f"Too many assertions ({len(assertions)} > 26)")
257
+ return False, warnings
258
+
259
+ # Warning if very few assertions from complex body
260
+ if len(assertions) < 2 and len(original.body) > 500:
261
+ warnings.append("Few assertions from large body - may have missed obligations")
262
+
263
+ is_valid = not any("missing SHALL" in w or "No assertions" in w for w in warnings)
264
+ return is_valid, warnings