elspais 0.11.1__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 (148) hide show
  1. elspais/__init__.py +2 -11
  2. elspais/{sponsors/__init__.py → associates.py} +102 -58
  3. elspais/cli.py +395 -79
  4. elspais/commands/__init__.py +9 -3
  5. elspais/commands/analyze.py +121 -173
  6. elspais/commands/changed.py +15 -30
  7. elspais/commands/config_cmd.py +13 -16
  8. elspais/commands/edit.py +60 -44
  9. elspais/commands/example_cmd.py +319 -0
  10. elspais/commands/hash_cmd.py +167 -183
  11. elspais/commands/health.py +1177 -0
  12. elspais/commands/index.py +98 -114
  13. elspais/commands/init.py +103 -26
  14. elspais/commands/reformat_cmd.py +41 -444
  15. elspais/commands/rules_cmd.py +7 -3
  16. elspais/commands/trace.py +444 -321
  17. elspais/commands/validate.py +195 -415
  18. elspais/config/__init__.py +799 -5
  19. elspais/{core/content_rules.py → content_rules.py} +20 -3
  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 +47 -29
  58. elspais/mcp/__main__.py +5 -1
  59. elspais/mcp/file_mutations.py +138 -0
  60. elspais/mcp/server.py +2016 -247
  61. elspais/testing/__init__.py +4 -4
  62. elspais/testing/config.py +3 -0
  63. elspais/testing/mapper.py +1 -1
  64. elspais/testing/result_parser.py +25 -21
  65. elspais/testing/scanner.py +301 -12
  66. elspais/utilities/__init__.py +1 -0
  67. elspais/utilities/docs_loader.py +115 -0
  68. elspais/utilities/git.py +607 -0
  69. elspais/{core → utilities}/hasher.py +8 -22
  70. elspais/utilities/md_renderer.py +189 -0
  71. elspais/{core → utilities}/patterns.py +58 -57
  72. elspais/utilities/reference_config.py +626 -0
  73. elspais/validation/__init__.py +19 -0
  74. elspais/validation/format.py +264 -0
  75. {elspais-0.11.1.dist-info → elspais-0.43.5.dist-info}/METADATA +7 -4
  76. elspais-0.43.5.dist-info/RECORD +80 -0
  77. elspais/config/defaults.py +0 -173
  78. elspais/config/loader.py +0 -494
  79. elspais/core/__init__.py +0 -21
  80. elspais/core/git.py +0 -352
  81. elspais/core/models.py +0 -320
  82. elspais/core/parser.py +0 -640
  83. elspais/core/rules.py +0 -514
  84. elspais/mcp/context.py +0 -171
  85. elspais/mcp/serializers.py +0 -112
  86. elspais/reformat/__init__.py +0 -50
  87. elspais/reformat/detector.py +0 -119
  88. elspais/reformat/hierarchy.py +0 -246
  89. elspais/reformat/line_breaks.py +0 -220
  90. elspais/reformat/prompts.py +0 -123
  91. elspais/reformat/transformer.py +0 -264
  92. elspais/trace_view/__init__.py +0 -54
  93. elspais/trace_view/coverage.py +0 -183
  94. elspais/trace_view/generators/__init__.py +0 -12
  95. elspais/trace_view/generators/base.py +0 -329
  96. elspais/trace_view/generators/csv.py +0 -122
  97. elspais/trace_view/generators/markdown.py +0 -175
  98. elspais/trace_view/html/__init__.py +0 -31
  99. elspais/trace_view/html/generator.py +0 -1006
  100. elspais/trace_view/html/templates/base.html +0 -283
  101. elspais/trace_view/html/templates/components/code_viewer_modal.html +0 -14
  102. elspais/trace_view/html/templates/components/file_picker_modal.html +0 -20
  103. elspais/trace_view/html/templates/components/legend_modal.html +0 -69
  104. elspais/trace_view/html/templates/components/review_panel.html +0 -118
  105. elspais/trace_view/html/templates/partials/review/help/help-panel.json +0 -244
  106. elspais/trace_view/html/templates/partials/review/help/onboarding.json +0 -77
  107. elspais/trace_view/html/templates/partials/review/help/tooltips.json +0 -237
  108. elspais/trace_view/html/templates/partials/review/review-comments.js +0 -928
  109. elspais/trace_view/html/templates/partials/review/review-data.js +0 -961
  110. elspais/trace_view/html/templates/partials/review/review-help.js +0 -679
  111. elspais/trace_view/html/templates/partials/review/review-init.js +0 -177
  112. elspais/trace_view/html/templates/partials/review/review-line-numbers.js +0 -429
  113. elspais/trace_view/html/templates/partials/review/review-packages.js +0 -1029
  114. elspais/trace_view/html/templates/partials/review/review-position.js +0 -540
  115. elspais/trace_view/html/templates/partials/review/review-resize.js +0 -115
  116. elspais/trace_view/html/templates/partials/review/review-status.js +0 -659
  117. elspais/trace_view/html/templates/partials/review/review-sync.js +0 -992
  118. elspais/trace_view/html/templates/partials/review-styles.css +0 -2238
  119. elspais/trace_view/html/templates/partials/scripts.js +0 -1741
  120. elspais/trace_view/html/templates/partials/styles.css +0 -1756
  121. elspais/trace_view/models.py +0 -353
  122. elspais/trace_view/review/__init__.py +0 -60
  123. elspais/trace_view/review/branches.py +0 -1149
  124. elspais/trace_view/review/models.py +0 -1205
  125. elspais/trace_view/review/position.py +0 -609
  126. elspais/trace_view/review/server.py +0 -1056
  127. elspais/trace_view/review/status.py +0 -470
  128. elspais/trace_view/review/storage.py +0 -1367
  129. elspais/trace_view/scanning.py +0 -213
  130. elspais/trace_view/specs/README.md +0 -84
  131. elspais/trace_view/specs/tv-d00001-template-architecture.md +0 -36
  132. elspais/trace_view/specs/tv-d00002-css-extraction.md +0 -37
  133. elspais/trace_view/specs/tv-d00003-js-extraction.md +0 -43
  134. elspais/trace_view/specs/tv-d00004-build-embedding.md +0 -40
  135. elspais/trace_view/specs/tv-d00005-test-format.md +0 -78
  136. elspais/trace_view/specs/tv-d00010-review-data-models.md +0 -33
  137. elspais/trace_view/specs/tv-d00011-review-storage.md +0 -33
  138. elspais/trace_view/specs/tv-d00012-position-resolution.md +0 -33
  139. elspais/trace_view/specs/tv-d00013-git-branches.md +0 -31
  140. elspais/trace_view/specs/tv-d00014-review-api-server.md +0 -31
  141. elspais/trace_view/specs/tv-d00015-status-modifier.md +0 -27
  142. elspais/trace_view/specs/tv-d00016-js-integration.md +0 -33
  143. elspais/trace_view/specs/tv-p00001-html-generator.md +0 -33
  144. elspais/trace_view/specs/tv-p00002-review-system.md +0 -29
  145. elspais-0.11.1.dist-info/RECORD +0 -101
  146. {elspais-0.11.1.dist-info → elspais-0.43.5.dist-info}/WHEEL +0 -0
  147. {elspais-0.11.1.dist-info → elspais-0.43.5.dist-info}/entry_points.txt +0 -0
  148. {elspais-0.11.1.dist-info → elspais-0.43.5.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,225 @@
1
+ """Heredocs parser for embedded requirement definitions.
2
+
3
+ This parser recognizes embedded requirement definitions in test files
4
+ (Python triple-quoted strings, shell heredocs) and claims them as
5
+ plain-text blocks, preventing the requirement parser from treating
6
+ them as real requirements.
7
+
8
+ If the text needs to be parsed later, it can be sent to the deserializer
9
+ as that can process arbitrary blocks of text.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import re
15
+ from pathlib import Path
16
+ from typing import TYPE_CHECKING, Iterator
17
+
18
+ from elspais.graph.parsers import ParsedContent
19
+
20
+ if TYPE_CHECKING:
21
+ from elspais.graph.parsers import ParseContext
22
+
23
+
24
+ def _get_file_type(file_path: str) -> str:
25
+ """Determine file type from path.
26
+
27
+ Args:
28
+ file_path: Path to the file.
29
+
30
+ Returns:
31
+ File type: 'python', 'shell', or 'other'.
32
+ """
33
+ suffix = Path(file_path).suffix.lower()
34
+ name = Path(file_path).name.lower()
35
+
36
+ if suffix == ".py":
37
+ return "python"
38
+ elif suffix in (".sh", ".bash", ".zsh", ".ksh"):
39
+ return "shell"
40
+ elif name.endswith(".sh") or name.endswith(".bash"):
41
+ return "shell"
42
+ else:
43
+ return "other"
44
+
45
+
46
+ class HeredocsParser:
47
+ """Parser for heredoc/multiline string blocks containing requirement patterns.
48
+
49
+ Priority: 10 (runs before requirement parser at 50)
50
+
51
+ Claims Python triple-quoted strings and shell heredocs that contain
52
+ REQ-xxx patterns, marking them as heredoc content type.
53
+ """
54
+
55
+ # Priority 10 - runs early to claim heredocs before requirement parser
56
+ priority: int = 10
57
+
58
+ # Pattern to detect requirement IDs
59
+ REQ_PATTERN = re.compile(r"REQ[-_][A-Za-z]?\d+", re.IGNORECASE)
60
+
61
+ # Python triple-quote start patterns
62
+ PYTHON_HEREDOC_START = re.compile(
63
+ r"^[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*[fFrRbBuU]?" # var = [prefix]
64
+ r'("""|\'\'\')', # triple quote
65
+ re.MULTILINE,
66
+ )
67
+
68
+ # Shell heredoc start pattern: << 'EOF' or << "EOF" or << EOF
69
+ SHELL_HEREDOC_START = re.compile(
70
+ r"<<\s*['\"]?([A-Za-z_][A-Za-z0-9_]*)['\"]?",
71
+ re.MULTILINE,
72
+ )
73
+
74
+ def claim_and_parse(
75
+ self,
76
+ lines: list[tuple[int, str]],
77
+ context: ParseContext,
78
+ ) -> Iterator[ParsedContent]:
79
+ """Claim heredoc blocks containing requirement patterns.
80
+
81
+ Args:
82
+ lines: List of (line_number, content) tuples.
83
+ context: Parse context with file information.
84
+
85
+ Yields:
86
+ ParsedContent for each claimed heredoc block.
87
+ """
88
+ file_type = _get_file_type(context.file_path)
89
+
90
+ if file_type == "python":
91
+ yield from self._parse_python_heredocs(lines, context)
92
+ elif file_type == "shell":
93
+ yield from self._parse_shell_heredocs(lines, context)
94
+ # Skip markdown, spec files, etc.
95
+
96
+ def _parse_python_heredocs(
97
+ self,
98
+ lines: list[tuple[int, str]],
99
+ context: ParseContext,
100
+ ) -> Iterator[ParsedContent]:
101
+ """Parse Python triple-quoted strings containing REQ patterns.
102
+
103
+ Args:
104
+ lines: List of (line_number, content) tuples.
105
+ context: Parse context.
106
+
107
+ Yields:
108
+ ParsedContent for each heredoc block.
109
+ """
110
+ i = 0
111
+ while i < len(lines):
112
+ line_num, content = lines[i]
113
+
114
+ # Check for triple-quote start
115
+ match = self.PYTHON_HEREDOC_START.match(content)
116
+ if match:
117
+ quote_char = match.group(1) # """ or '''
118
+
119
+ # Find the end of the heredoc
120
+ heredoc_lines = [content]
121
+ end_idx = i
122
+
123
+ # Check if it's a one-liner
124
+ rest = content[match.end() :]
125
+ if quote_char in rest:
126
+ # Single line heredoc - check for REQ pattern
127
+ if self.REQ_PATTERN.search(content):
128
+ yield ParsedContent(
129
+ content_type="heredoc",
130
+ start_line=line_num,
131
+ end_line=line_num,
132
+ raw_text=content,
133
+ parsed_data={"quote_style": quote_char},
134
+ )
135
+ i += 1
136
+ continue
137
+
138
+ # Multi-line heredoc
139
+ for j in range(i + 1, len(lines)):
140
+ end_line_num, end_content = lines[j]
141
+ heredoc_lines.append(end_content)
142
+ if quote_char in end_content:
143
+ end_idx = j
144
+ break
145
+ else:
146
+ # No closing quote found - skip
147
+ i += 1
148
+ continue
149
+
150
+ # Check if heredoc contains REQ pattern
151
+ full_text = "\n".join(heredoc_lines)
152
+ if self.REQ_PATTERN.search(full_text):
153
+ yield ParsedContent(
154
+ content_type="heredoc",
155
+ start_line=line_num,
156
+ end_line=lines[end_idx][0],
157
+ raw_text=full_text,
158
+ parsed_data={"quote_style": quote_char},
159
+ )
160
+ i = end_idx + 1
161
+ continue
162
+
163
+ i += 1
164
+
165
+ def _parse_shell_heredocs(
166
+ self,
167
+ lines: list[tuple[int, str]],
168
+ context: ParseContext,
169
+ ) -> Iterator[ParsedContent]:
170
+ """Parse shell-style heredocs containing REQ patterns.
171
+
172
+ Args:
173
+ lines: List of (line_number, content) tuples.
174
+ context: Parse context.
175
+
176
+ Yields:
177
+ ParsedContent for each heredoc block.
178
+ """
179
+ i = 0
180
+ while i < len(lines):
181
+ line_num, content = lines[i]
182
+
183
+ # Check for shell heredoc start
184
+ match = self.SHELL_HEREDOC_START.search(content)
185
+ if match:
186
+ delimiter = match.group(1) # e.g., EOF, END
187
+
188
+ # Find the end delimiter
189
+ heredoc_lines = [content]
190
+ end_idx = i
191
+
192
+ for j in range(i + 1, len(lines)):
193
+ end_line_num, end_content = lines[j]
194
+ heredoc_lines.append(end_content)
195
+ if end_content.strip() == delimiter:
196
+ end_idx = j
197
+ break
198
+ else:
199
+ # No closing delimiter found - skip
200
+ i += 1
201
+ continue
202
+
203
+ # Check if heredoc contains REQ pattern
204
+ full_text = "\n".join(heredoc_lines)
205
+ if self.REQ_PATTERN.search(full_text):
206
+ yield ParsedContent(
207
+ content_type="heredoc",
208
+ start_line=line_num,
209
+ end_line=lines[end_idx][0],
210
+ raw_text=full_text,
211
+ parsed_data={"delimiter": delimiter},
212
+ )
213
+ i = end_idx + 1
214
+ continue
215
+
216
+ i += 1
217
+
218
+
219
+ def create_parser() -> HeredocsParser:
220
+ """Factory function to create a HeredocsParser.
221
+
222
+ Returns:
223
+ New HeredocsParser instance.
224
+ """
225
+ return HeredocsParser()
@@ -0,0 +1,131 @@
1
+ """JourneyParser - Priority 60 parser for user journey blocks.
2
+
3
+ Parses user journey specifications from markdown.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ from typing import Any, Iterator
10
+
11
+ from elspais.graph.parsers import ParseContext, ParsedContent
12
+
13
+
14
+ class JourneyParser:
15
+ """Parser for user journey blocks.
16
+
17
+ Priority: 60 (after requirements)
18
+
19
+ Parses user journeys in format:
20
+ - Header: ## JNY-xxx-NN: Title
21
+ - Actor/Goal fields
22
+ - Steps section
23
+ - End marker
24
+ """
25
+
26
+ priority = 60
27
+
28
+ # Journey ID pattern: JNY-{Descriptor}-{number}
29
+ HEADER_PATTERN = re.compile(r"^#*\s*(?P<id>JNY-[A-Za-z0-9-]+):\s*(?P<title>.+)$")
30
+ ACTOR_PATTERN = re.compile(r"\*\*Actor\*\*:\s*(?P<actor>.+?)(?:\n|$)")
31
+ GOAL_PATTERN = re.compile(r"\*\*Goal\*\*:\s*(?P<goal>.+?)(?:\n|$)")
32
+ END_MARKER_PATTERN = re.compile(r"^\*End\*\s+\*JNY-[^*]+\*", re.MULTILINE)
33
+
34
+ def claim_and_parse(
35
+ self,
36
+ lines: list[tuple[int, str]],
37
+ context: ParseContext,
38
+ ) -> Iterator[ParsedContent]:
39
+ """Claim and parse user journey blocks.
40
+
41
+ Args:
42
+ lines: List of (line_number, content) tuples.
43
+ context: Parsing context.
44
+
45
+ Yields:
46
+ ParsedContent for each journey block.
47
+ """
48
+ line_map = dict(lines)
49
+ line_numbers = sorted(line_map.keys())
50
+
51
+ if not line_numbers:
52
+ return
53
+
54
+ claimed: set[int] = set()
55
+ i = 0
56
+
57
+ while i < len(line_numbers):
58
+ ln = line_numbers[i]
59
+ if ln in claimed:
60
+ i += 1
61
+ continue
62
+
63
+ text = line_map[ln]
64
+
65
+ header_match = self.HEADER_PATTERN.match(text)
66
+ if header_match:
67
+ journey_id = header_match.group("id")
68
+ title = header_match.group("title").strip()
69
+ start_line = ln
70
+
71
+ # Find end of journey
72
+ journey_lines = [(ln, text)]
73
+ end_line = ln
74
+ j = i + 1
75
+
76
+ while j < len(line_numbers):
77
+ next_ln = line_numbers[j]
78
+ next_text = line_map[next_ln]
79
+ journey_lines.append((next_ln, next_text))
80
+ end_line = next_ln
81
+
82
+ if self.END_MARKER_PATTERN.match(next_text):
83
+ j += 1
84
+ break
85
+
86
+ # Next journey header
87
+ if self.HEADER_PATTERN.match(next_text):
88
+ journey_lines.pop()
89
+ end_line = line_numbers[j - 1] if j > i + 1 else ln
90
+ break
91
+
92
+ j += 1
93
+
94
+ # Claim lines
95
+ for claim_ln, _ in journey_lines:
96
+ claimed.add(claim_ln)
97
+
98
+ raw_text = "\n".join(t for _, t in journey_lines)
99
+ parsed_data = self._parse_journey(journey_id, title, raw_text)
100
+
101
+ yield ParsedContent(
102
+ content_type="journey",
103
+ start_line=start_line,
104
+ end_line=end_line,
105
+ raw_text=raw_text,
106
+ parsed_data=parsed_data,
107
+ )
108
+
109
+ while i < len(line_numbers) and line_numbers[i] in claimed:
110
+ i += 1
111
+ else:
112
+ i += 1
113
+
114
+ def _parse_journey(self, journey_id: str, title: str, text: str) -> dict[str, Any]:
115
+ """Parse journey fields from text block."""
116
+ data: dict[str, Any] = {
117
+ "id": journey_id,
118
+ "title": title,
119
+ "actor": None,
120
+ "goal": None,
121
+ }
122
+
123
+ actor_match = self.ACTOR_PATTERN.search(text)
124
+ if actor_match:
125
+ data["actor"] = actor_match.group("actor").strip()
126
+
127
+ goal_match = self.GOAL_PATTERN.search(text)
128
+ if goal_match:
129
+ data["goal"] = goal_match.group("goal").strip()
130
+
131
+ return data
@@ -0,0 +1,79 @@
1
+ """RemainderParser - Priority 999 catch-all parser.
2
+
3
+ Claims all remaining unclaimed lines after all other parsers have run.
4
+ Groups contiguous lines into remainder blocks.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Iterator
10
+
11
+ from elspais.graph.parsers import ParseContext, ParsedContent
12
+
13
+
14
+ class RemainderParser:
15
+ """Parser for unclaimed remainder content.
16
+
17
+ Priority: 999 (lowest priority, runs last)
18
+
19
+ Claims all remaining lines that no other parser claimed, grouping
20
+ contiguous lines into blocks. This ensures every line in the file
21
+ is assigned to exactly one ParsedContent.
22
+ """
23
+
24
+ priority = 999
25
+
26
+ def claim_and_parse(
27
+ self,
28
+ lines: list[tuple[int, str]],
29
+ context: ParseContext,
30
+ ) -> Iterator[ParsedContent]:
31
+ """Claim all remaining lines as remainder blocks.
32
+
33
+ Groups contiguous lines (no gaps in line numbers) into single
34
+ remainder blocks.
35
+
36
+ Args:
37
+ lines: List of (line_number, content) tuples.
38
+ context: Parsing context.
39
+
40
+ Yields:
41
+ ParsedContent for each contiguous remainder block.
42
+ """
43
+ if not lines:
44
+ return
45
+
46
+ # Sort by line number
47
+ sorted_lines = sorted(lines, key=lambda x: x[0])
48
+
49
+ # Group contiguous lines
50
+ groups: list[list[tuple[int, str]]] = []
51
+ current_group: list[tuple[int, str]] = []
52
+
53
+ for ln, text in sorted_lines:
54
+ if not current_group:
55
+ current_group.append((ln, text))
56
+ elif ln == current_group[-1][0] + 1:
57
+ # Contiguous
58
+ current_group.append((ln, text))
59
+ else:
60
+ # Gap - start new group
61
+ groups.append(current_group)
62
+ current_group = [(ln, text)]
63
+
64
+ if current_group:
65
+ groups.append(current_group)
66
+
67
+ # Yield each group as a remainder block
68
+ for group in groups:
69
+ start_line = group[0][0]
70
+ end_line = group[-1][0]
71
+ raw_text = "\n".join(text for _, text in group)
72
+
73
+ yield ParsedContent(
74
+ content_type="remainder",
75
+ start_line=start_line,
76
+ end_line=end_line,
77
+ raw_text=raw_text,
78
+ parsed_data={},
79
+ )