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.
- elspais/__init__.py +1 -10
- elspais/{sponsors/__init__.py → associates.py} +102 -56
- elspais/cli.py +366 -69
- elspais/commands/__init__.py +9 -3
- elspais/commands/analyze.py +118 -169
- elspais/commands/changed.py +12 -23
- elspais/commands/config_cmd.py +10 -13
- elspais/commands/edit.py +33 -13
- elspais/commands/example_cmd.py +319 -0
- elspais/commands/hash_cmd.py +161 -183
- elspais/commands/health.py +1177 -0
- elspais/commands/index.py +98 -115
- elspais/commands/init.py +99 -22
- elspais/commands/reformat_cmd.py +41 -433
- elspais/commands/rules_cmd.py +2 -2
- elspais/commands/trace.py +443 -324
- elspais/commands/validate.py +193 -411
- elspais/config/__init__.py +799 -5
- elspais/{core/content_rules.py → content_rules.py} +20 -2
- elspais/docs/cli/assertions.md +67 -0
- elspais/docs/cli/commands.md +304 -0
- elspais/docs/cli/config.md +262 -0
- elspais/docs/cli/format.md +66 -0
- elspais/docs/cli/git.md +45 -0
- elspais/docs/cli/health.md +190 -0
- elspais/docs/cli/hierarchy.md +60 -0
- elspais/docs/cli/ignore.md +72 -0
- elspais/docs/cli/mcp.md +245 -0
- elspais/docs/cli/quickstart.md +58 -0
- elspais/docs/cli/traceability.md +89 -0
- elspais/docs/cli/validation.md +96 -0
- elspais/graph/GraphNode.py +383 -0
- elspais/graph/__init__.py +40 -0
- elspais/graph/annotators.py +927 -0
- elspais/graph/builder.py +1886 -0
- elspais/graph/deserializer.py +248 -0
- elspais/graph/factory.py +284 -0
- elspais/graph/metrics.py +127 -0
- elspais/graph/mutations.py +161 -0
- elspais/graph/parsers/__init__.py +156 -0
- elspais/graph/parsers/code.py +213 -0
- elspais/graph/parsers/comments.py +112 -0
- elspais/graph/parsers/config_helpers.py +29 -0
- elspais/graph/parsers/heredocs.py +225 -0
- elspais/graph/parsers/journey.py +131 -0
- elspais/graph/parsers/remainder.py +79 -0
- elspais/graph/parsers/requirement.py +347 -0
- elspais/graph/parsers/results/__init__.py +6 -0
- elspais/graph/parsers/results/junit_xml.py +229 -0
- elspais/graph/parsers/results/pytest_json.py +313 -0
- elspais/graph/parsers/test.py +305 -0
- elspais/graph/relations.py +78 -0
- elspais/graph/serialize.py +216 -0
- elspais/html/__init__.py +8 -0
- elspais/html/generator.py +731 -0
- elspais/html/templates/trace_view.html.j2 +2151 -0
- elspais/mcp/__init__.py +45 -29
- elspais/mcp/__main__.py +5 -1
- elspais/mcp/file_mutations.py +138 -0
- elspais/mcp/server.py +1998 -244
- elspais/testing/__init__.py +3 -3
- elspais/testing/config.py +3 -0
- elspais/testing/mapper.py +1 -1
- elspais/testing/scanner.py +301 -12
- elspais/utilities/__init__.py +1 -0
- elspais/utilities/docs_loader.py +115 -0
- elspais/utilities/git.py +607 -0
- elspais/{core → utilities}/hasher.py +8 -22
- elspais/utilities/md_renderer.py +189 -0
- elspais/{core → utilities}/patterns.py +56 -51
- elspais/utilities/reference_config.py +626 -0
- elspais/validation/__init__.py +19 -0
- elspais/validation/format.py +264 -0
- {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/METADATA +7 -4
- elspais-0.43.5.dist-info/RECORD +80 -0
- elspais/config/defaults.py +0 -179
- elspais/config/loader.py +0 -494
- elspais/core/__init__.py +0 -21
- elspais/core/git.py +0 -346
- elspais/core/models.py +0 -320
- elspais/core/parser.py +0 -639
- elspais/core/rules.py +0 -509
- elspais/mcp/context.py +0 -172
- elspais/mcp/serializers.py +0 -112
- elspais/reformat/__init__.py +0 -50
- elspais/reformat/detector.py +0 -112
- elspais/reformat/hierarchy.py +0 -247
- elspais/reformat/line_breaks.py +0 -218
- elspais/reformat/prompts.py +0 -133
- elspais/reformat/transformer.py +0 -266
- elspais/trace_view/__init__.py +0 -55
- elspais/trace_view/coverage.py +0 -183
- elspais/trace_view/generators/__init__.py +0 -12
- elspais/trace_view/generators/base.py +0 -334
- elspais/trace_view/generators/csv.py +0 -118
- elspais/trace_view/generators/markdown.py +0 -170
- elspais/trace_view/html/__init__.py +0 -33
- elspais/trace_view/html/generator.py +0 -1140
- elspais/trace_view/html/templates/base.html +0 -283
- elspais/trace_view/html/templates/components/code_viewer_modal.html +0 -14
- elspais/trace_view/html/templates/components/file_picker_modal.html +0 -20
- elspais/trace_view/html/templates/components/legend_modal.html +0 -69
- elspais/trace_view/html/templates/components/review_panel.html +0 -118
- elspais/trace_view/html/templates/partials/review/help/help-panel.json +0 -244
- elspais/trace_view/html/templates/partials/review/help/onboarding.json +0 -77
- elspais/trace_view/html/templates/partials/review/help/tooltips.json +0 -237
- elspais/trace_view/html/templates/partials/review/review-comments.js +0 -928
- elspais/trace_view/html/templates/partials/review/review-data.js +0 -961
- elspais/trace_view/html/templates/partials/review/review-help.js +0 -679
- elspais/trace_view/html/templates/partials/review/review-init.js +0 -177
- elspais/trace_view/html/templates/partials/review/review-line-numbers.js +0 -429
- elspais/trace_view/html/templates/partials/review/review-packages.js +0 -1029
- elspais/trace_view/html/templates/partials/review/review-position.js +0 -540
- elspais/trace_view/html/templates/partials/review/review-resize.js +0 -115
- elspais/trace_view/html/templates/partials/review/review-status.js +0 -659
- elspais/trace_view/html/templates/partials/review/review-sync.js +0 -992
- elspais/trace_view/html/templates/partials/review-styles.css +0 -2238
- elspais/trace_view/html/templates/partials/scripts.js +0 -1741
- elspais/trace_view/html/templates/partials/styles.css +0 -1756
- elspais/trace_view/models.py +0 -378
- elspais/trace_view/review/__init__.py +0 -63
- elspais/trace_view/review/branches.py +0 -1142
- elspais/trace_view/review/models.py +0 -1200
- elspais/trace_view/review/position.py +0 -591
- elspais/trace_view/review/server.py +0 -1032
- elspais/trace_view/review/status.py +0 -455
- elspais/trace_view/review/storage.py +0 -1343
- elspais/trace_view/scanning.py +0 -213
- elspais/trace_view/specs/README.md +0 -84
- elspais/trace_view/specs/tv-d00001-template-architecture.md +0 -36
- elspais/trace_view/specs/tv-d00002-css-extraction.md +0 -37
- elspais/trace_view/specs/tv-d00003-js-extraction.md +0 -43
- elspais/trace_view/specs/tv-d00004-build-embedding.md +0 -40
- elspais/trace_view/specs/tv-d00005-test-format.md +0 -78
- elspais/trace_view/specs/tv-d00010-review-data-models.md +0 -33
- elspais/trace_view/specs/tv-d00011-review-storage.md +0 -33
- elspais/trace_view/specs/tv-d00012-position-resolution.md +0 -33
- elspais/trace_view/specs/tv-d00013-git-branches.md +0 -31
- elspais/trace_view/specs/tv-d00014-review-api-server.md +0 -31
- elspais/trace_view/specs/tv-d00015-status-modifier.md +0 -27
- elspais/trace_view/specs/tv-d00016-js-integration.md +0 -33
- elspais/trace_view/specs/tv-p00001-html-generator.md +0 -33
- elspais/trace_view/specs/tv-p00002-review-system.md +0 -29
- elspais-0.11.2.dist-info/RECORD +0 -101
- {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/WHEEL +0 -0
- {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/entry_points.txt +0 -0
- {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
|
+
)
|