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
@@ -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
+ )