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,626 @@
|
|
|
1
|
+
"""Unified reference configuration for all parsers.
|
|
2
|
+
|
|
3
|
+
This module provides configurable reference pattern matching used by:
|
|
4
|
+
- CodeParser: # Implements: REQ-xxx
|
|
5
|
+
- TestParser: def test_REQ_xxx() and # Tests: REQ-xxx
|
|
6
|
+
- JUnitXMLParser: test names containing REQ-xxx
|
|
7
|
+
- PytestJSONParser: test names containing REQ-xxx
|
|
8
|
+
|
|
9
|
+
The configuration supports:
|
|
10
|
+
- Default patterns applied to all files
|
|
11
|
+
- File-type specific overrides (*.py, *.java, etc.)
|
|
12
|
+
- Directory-based overrides (tests/legacy/**)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import fnmatch
|
|
18
|
+
import re
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import TYPE_CHECKING, Any
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from elspais.utilities.patterns import PatternConfig
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class ReferenceConfig:
|
|
29
|
+
"""Configuration for reference pattern matching.
|
|
30
|
+
|
|
31
|
+
Used by all parsers: TestParser, CodeParser, JUnitXMLParser, PytestJSONParser.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
separators: Separator characters to accept between ID components (default: ["-", "_"])
|
|
35
|
+
case_sensitive: Whether matching is case-sensitive (default: False)
|
|
36
|
+
prefix_optional: Whether the prefix (e.g., "REQ") is required (default: False)
|
|
37
|
+
comment_styles: Recognized comment markers (default: ["#", "//", "--"])
|
|
38
|
+
keywords: Keywords for different reference types
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
separators: list[str] = field(default_factory=lambda: ["-", "_"])
|
|
42
|
+
case_sensitive: bool = False
|
|
43
|
+
prefix_optional: bool = False
|
|
44
|
+
comment_styles: list[str] = field(default_factory=lambda: ["#", "//", "--"])
|
|
45
|
+
keywords: dict[str, list[str]] = field(
|
|
46
|
+
default_factory=lambda: {
|
|
47
|
+
"implements": ["Implements", "IMPLEMENTS"],
|
|
48
|
+
"validates": ["Validates", "Tests", "VALIDATES", "TESTS"],
|
|
49
|
+
"refines": ["Refines", "REFINES"],
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def from_dict(cls, data: dict[str, Any]) -> ReferenceConfig:
|
|
55
|
+
"""Create ReferenceConfig from configuration dictionary.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
data: Dictionary with optional keys: separators, case_sensitive,
|
|
59
|
+
prefix_optional, comment_styles, keywords
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
ReferenceConfig instance with values from dict or defaults
|
|
63
|
+
"""
|
|
64
|
+
return cls(
|
|
65
|
+
separators=data.get("separators", ["-", "_"]),
|
|
66
|
+
case_sensitive=data.get("case_sensitive", False),
|
|
67
|
+
prefix_optional=data.get("prefix_optional", False),
|
|
68
|
+
comment_styles=data.get("comment_styles", ["#", "//", "--"]),
|
|
69
|
+
keywords=data.get(
|
|
70
|
+
"keywords",
|
|
71
|
+
{
|
|
72
|
+
"implements": ["Implements", "IMPLEMENTS"],
|
|
73
|
+
"validates": ["Validates", "Tests", "VALIDATES", "TESTS"],
|
|
74
|
+
"refines": ["Refines", "REFINES"],
|
|
75
|
+
},
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def merge_with(self, override: ReferenceOverride) -> ReferenceConfig:
|
|
80
|
+
"""Create a new config by merging this config with an override.
|
|
81
|
+
|
|
82
|
+
Only non-None values from the override are applied.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
override: ReferenceOverride with values to apply
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
New ReferenceConfig with merged values
|
|
89
|
+
"""
|
|
90
|
+
# Start with current values
|
|
91
|
+
merged_keywords = dict(self.keywords)
|
|
92
|
+
|
|
93
|
+
# Merge keywords if override has them
|
|
94
|
+
if override.keywords is not None:
|
|
95
|
+
merged_keywords.update(override.keywords)
|
|
96
|
+
|
|
97
|
+
return ReferenceConfig(
|
|
98
|
+
separators=override.separators if override.separators is not None else self.separators,
|
|
99
|
+
case_sensitive=(
|
|
100
|
+
override.case_sensitive
|
|
101
|
+
if override.case_sensitive is not None
|
|
102
|
+
else self.case_sensitive
|
|
103
|
+
),
|
|
104
|
+
prefix_optional=(
|
|
105
|
+
override.prefix_optional
|
|
106
|
+
if override.prefix_optional is not None
|
|
107
|
+
else self.prefix_optional
|
|
108
|
+
),
|
|
109
|
+
comment_styles=(
|
|
110
|
+
override.comment_styles
|
|
111
|
+
if override.comment_styles is not None
|
|
112
|
+
else self.comment_styles
|
|
113
|
+
),
|
|
114
|
+
keywords=merged_keywords,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class ReferenceOverride:
|
|
120
|
+
"""Override rule for specific file types or directories.
|
|
121
|
+
|
|
122
|
+
Attributes:
|
|
123
|
+
match: Glob pattern to match files (e.g., "*.py", "tests/legacy/**")
|
|
124
|
+
separators: Override separator characters (None = use default)
|
|
125
|
+
case_sensitive: Override case sensitivity (None = use default)
|
|
126
|
+
prefix_optional: Override prefix requirement (None = use default)
|
|
127
|
+
comment_styles: Override comment styles (None = use default)
|
|
128
|
+
keywords: Override keywords dict (None = use default)
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
match: str
|
|
132
|
+
separators: list[str] | None = None
|
|
133
|
+
case_sensitive: bool | None = None
|
|
134
|
+
prefix_optional: bool | None = None
|
|
135
|
+
comment_styles: list[str] | None = None
|
|
136
|
+
keywords: dict[str, list[str]] | None = None
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def from_dict(cls, data: dict[str, Any]) -> ReferenceOverride:
|
|
140
|
+
"""Create ReferenceOverride from configuration dictionary.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
data: Dictionary with 'match' (required) and optional override keys
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
ReferenceOverride instance
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
ValueError: If 'match' key is missing
|
|
150
|
+
"""
|
|
151
|
+
if "match" not in data:
|
|
152
|
+
raise ValueError("ReferenceOverride requires 'match' pattern")
|
|
153
|
+
|
|
154
|
+
return cls(
|
|
155
|
+
match=data["match"],
|
|
156
|
+
separators=data.get("separators"),
|
|
157
|
+
case_sensitive=data.get("case_sensitive"),
|
|
158
|
+
prefix_optional=data.get("prefix_optional"),
|
|
159
|
+
comment_styles=data.get("comment_styles"),
|
|
160
|
+
keywords=data.get("keywords"),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def applies_to(self, file_path: Path, base_path: Path) -> bool:
|
|
164
|
+
"""Check if this override applies to the given file.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
file_path: Absolute path to the file being checked
|
|
168
|
+
base_path: Base path (repo root) for relative matching
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
True if the match pattern matches the file
|
|
172
|
+
"""
|
|
173
|
+
# Get relative path for matching
|
|
174
|
+
try:
|
|
175
|
+
rel_path = file_path.relative_to(base_path)
|
|
176
|
+
except ValueError:
|
|
177
|
+
# file_path is not under base_path, use filename only
|
|
178
|
+
rel_path = Path(file_path.name)
|
|
179
|
+
|
|
180
|
+
# Convert to string for fnmatch
|
|
181
|
+
rel_str = str(rel_path)
|
|
182
|
+
|
|
183
|
+
# Handle both forward and backward slashes on Windows
|
|
184
|
+
rel_str_normalized = rel_str.replace("\\", "/")
|
|
185
|
+
|
|
186
|
+
# Check if pattern matches
|
|
187
|
+
# fnmatch doesn't handle ** well, so we need special handling
|
|
188
|
+
pattern = self.match
|
|
189
|
+
|
|
190
|
+
if "**" in pattern:
|
|
191
|
+
# For ** patterns, we need recursive matching
|
|
192
|
+
# Split pattern into parts and match recursively
|
|
193
|
+
return self._match_recursive(rel_str_normalized, pattern)
|
|
194
|
+
else:
|
|
195
|
+
# Simple glob - check both full path and just filename
|
|
196
|
+
if fnmatch.fnmatch(rel_str_normalized, pattern):
|
|
197
|
+
return True
|
|
198
|
+
# Also try matching just the filename for patterns like "*.py"
|
|
199
|
+
if fnmatch.fnmatch(file_path.name, pattern):
|
|
200
|
+
return True
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
def _match_recursive(self, path: str, pattern: str) -> bool:
|
|
204
|
+
"""Match a path against a pattern containing **.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
path: Normalized file path (forward slashes)
|
|
208
|
+
pattern: Glob pattern with ** for recursive matching
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
True if pattern matches path
|
|
212
|
+
"""
|
|
213
|
+
# Split on **
|
|
214
|
+
parts = pattern.split("**")
|
|
215
|
+
|
|
216
|
+
if len(parts) == 2:
|
|
217
|
+
# Pattern like "tests/**" or "**/test.py" or "tests/**/fixtures"
|
|
218
|
+
prefix, suffix = parts
|
|
219
|
+
|
|
220
|
+
# Remove leading/trailing slashes from parts
|
|
221
|
+
prefix = prefix.rstrip("/")
|
|
222
|
+
suffix = suffix.lstrip("/")
|
|
223
|
+
|
|
224
|
+
if prefix and suffix:
|
|
225
|
+
# Pattern like "tests/**/fixtures/*.py"
|
|
226
|
+
# Path must start with prefix and end matching suffix
|
|
227
|
+
if not path.startswith(prefix + "/") and path != prefix:
|
|
228
|
+
return False
|
|
229
|
+
# Get the part after prefix
|
|
230
|
+
remaining = path[len(prefix) :].lstrip("/")
|
|
231
|
+
# Check if any suffix of remaining matches the suffix pattern
|
|
232
|
+
parts_list = remaining.split("/")
|
|
233
|
+
for i in range(len(parts_list)):
|
|
234
|
+
candidate = "/".join(parts_list[i:])
|
|
235
|
+
if fnmatch.fnmatch(candidate, suffix):
|
|
236
|
+
return True
|
|
237
|
+
return False
|
|
238
|
+
elif prefix:
|
|
239
|
+
# Pattern like "tests/**" - match anything under tests/
|
|
240
|
+
return path.startswith(prefix + "/") or path == prefix
|
|
241
|
+
elif suffix:
|
|
242
|
+
# Pattern like "**/test.py" - match file anywhere
|
|
243
|
+
# Check if path ends with suffix or matches suffix directly
|
|
244
|
+
if fnmatch.fnmatch(path, suffix):
|
|
245
|
+
return True
|
|
246
|
+
# Check if any path component matches
|
|
247
|
+
for i in range(len(path.split("/"))):
|
|
248
|
+
candidate = "/".join(path.split("/")[i:])
|
|
249
|
+
if fnmatch.fnmatch(candidate, suffix):
|
|
250
|
+
return True
|
|
251
|
+
return False
|
|
252
|
+
else:
|
|
253
|
+
# Just "**" - matches everything
|
|
254
|
+
return True
|
|
255
|
+
|
|
256
|
+
# Multiple ** in pattern - complex case, fall back to basic matching
|
|
257
|
+
return fnmatch.fnmatch(path, pattern)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class ReferenceResolver:
|
|
261
|
+
"""Resolves which reference config to use for a given file.
|
|
262
|
+
|
|
263
|
+
This is the SINGLE entry point for all parsers to get their configuration.
|
|
264
|
+
It applies defaults and matching overrides in order.
|
|
265
|
+
|
|
266
|
+
Example:
|
|
267
|
+
resolver = ReferenceResolver(defaults, overrides)
|
|
268
|
+
config = resolver.resolve(Path("tests/test_auth.py"), repo_root)
|
|
269
|
+
# config now has merged defaults + any matching overrides
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
def __init__(self, defaults: ReferenceConfig, overrides: list[ReferenceOverride] | None = None):
|
|
273
|
+
"""Initialize the resolver.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
defaults: Default configuration to use
|
|
277
|
+
overrides: Optional list of override rules (applied in order)
|
|
278
|
+
"""
|
|
279
|
+
self.defaults = defaults
|
|
280
|
+
self.overrides = overrides or []
|
|
281
|
+
|
|
282
|
+
@classmethod
|
|
283
|
+
def from_config(cls, config: dict[str, Any]) -> ReferenceResolver:
|
|
284
|
+
"""Create ReferenceResolver from the references config section.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
config: The 'references' section from elspais config
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Configured ReferenceResolver
|
|
291
|
+
"""
|
|
292
|
+
defaults_dict = config.get("defaults", {})
|
|
293
|
+
overrides_list = config.get("overrides", [])
|
|
294
|
+
|
|
295
|
+
defaults = ReferenceConfig.from_dict(defaults_dict)
|
|
296
|
+
overrides = [ReferenceOverride.from_dict(o) for o in overrides_list]
|
|
297
|
+
|
|
298
|
+
return cls(defaults, overrides)
|
|
299
|
+
|
|
300
|
+
def resolve(self, file_path: Path, base_path: Path) -> ReferenceConfig:
|
|
301
|
+
"""Return merged config for file (defaults + matching overrides).
|
|
302
|
+
|
|
303
|
+
Overrides are applied in order, so later matching overrides
|
|
304
|
+
take precedence over earlier ones.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
file_path: Path to the file being processed
|
|
308
|
+
base_path: Base path (repo root) for relative matching
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
ReferenceConfig with all applicable overrides merged
|
|
312
|
+
"""
|
|
313
|
+
result = self.defaults
|
|
314
|
+
|
|
315
|
+
for override in self.overrides:
|
|
316
|
+
if override.applies_to(file_path, base_path):
|
|
317
|
+
result = result.merge_with(override)
|
|
318
|
+
|
|
319
|
+
return result
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# =============================================================================
|
|
323
|
+
# Pattern Builder Functions
|
|
324
|
+
# =============================================================================
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def build_id_pattern(
|
|
328
|
+
pattern_config: PatternConfig,
|
|
329
|
+
ref_config: ReferenceConfig,
|
|
330
|
+
include_assertion: bool = True,
|
|
331
|
+
) -> re.Pattern[str]:
|
|
332
|
+
"""Build regex pattern for matching requirement IDs.
|
|
333
|
+
|
|
334
|
+
Creates a pattern that matches requirement IDs based on the configured
|
|
335
|
+
PatternConfig (for ID structure) and ReferenceConfig (for separators, etc.)
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
pattern_config: Configuration for ID structure (prefix, types, format)
|
|
339
|
+
ref_config: Configuration for reference matching (separators, case sensitivity)
|
|
340
|
+
include_assertion: Whether to include optional assertion suffix
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
Compiled regex pattern for matching requirement IDs
|
|
344
|
+
"""
|
|
345
|
+
# Get prefix from pattern_config
|
|
346
|
+
prefix = pattern_config.prefix
|
|
347
|
+
|
|
348
|
+
# Build separator pattern from ref_config
|
|
349
|
+
sep_pattern = _build_separator_pattern(ref_config.separators)
|
|
350
|
+
|
|
351
|
+
# Get type codes from pattern_config
|
|
352
|
+
type_codes = pattern_config.get_all_type_ids()
|
|
353
|
+
if type_codes:
|
|
354
|
+
type_pattern = f"(?:{'|'.join(re.escape(t) for t in type_codes)})"
|
|
355
|
+
else:
|
|
356
|
+
type_pattern = r"[a-z]" # Default: single lowercase letter
|
|
357
|
+
|
|
358
|
+
# Get ID format
|
|
359
|
+
id_format = pattern_config.id_format
|
|
360
|
+
style = id_format.get("style", "numeric")
|
|
361
|
+
digits = id_format.get("digits", 5)
|
|
362
|
+
|
|
363
|
+
if style == "numeric":
|
|
364
|
+
id_number_pattern = rf"\d{{{digits}}}"
|
|
365
|
+
else:
|
|
366
|
+
id_number_pattern = r"[A-Za-z0-9]+"
|
|
367
|
+
|
|
368
|
+
# Build assertion pattern if needed
|
|
369
|
+
assertion_pattern = ""
|
|
370
|
+
if include_assertion:
|
|
371
|
+
# Assertion labels are typically uppercase letters, possibly multiple
|
|
372
|
+
# IMPORTANT: Add negative lookahead (?![a-z]) to prevent matching
|
|
373
|
+
# lowercase letters that are part of longer words.
|
|
374
|
+
# e.g., test_REQ_p00001_login should NOT capture "l" as assertion
|
|
375
|
+
assertion_label = pattern_config.get_assertion_label_pattern()
|
|
376
|
+
if assertion_label:
|
|
377
|
+
# Make assertion optional with separator, with negative lookahead
|
|
378
|
+
assertion_pattern = rf"(?:{sep_pattern}(?P<assertion>{assertion_label})(?![a-z]))?"
|
|
379
|
+
else:
|
|
380
|
+
# Default: single uppercase letter, with negative lookahead
|
|
381
|
+
assertion_pattern = rf"(?:{sep_pattern}(?P<assertion>[A-Z])(?![a-z]))?"
|
|
382
|
+
|
|
383
|
+
# Build the full pattern
|
|
384
|
+
if ref_config.prefix_optional:
|
|
385
|
+
prefix_pattern = rf"(?:{re.escape(prefix)}{sep_pattern})?"
|
|
386
|
+
else:
|
|
387
|
+
prefix_pattern = rf"{re.escape(prefix)}{sep_pattern}"
|
|
388
|
+
|
|
389
|
+
full_pattern = (
|
|
390
|
+
rf"(?P<full_id>{prefix_pattern}"
|
|
391
|
+
rf"(?P<type>{type_pattern})"
|
|
392
|
+
rf"(?P<number>{id_number_pattern})"
|
|
393
|
+
rf"{assertion_pattern})"
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
flags = 0 if ref_config.case_sensitive else re.IGNORECASE
|
|
397
|
+
return re.compile(full_pattern, flags)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def build_comment_pattern(
|
|
401
|
+
pattern_config: PatternConfig,
|
|
402
|
+
ref_config: ReferenceConfig,
|
|
403
|
+
keyword_type: str = "implements",
|
|
404
|
+
) -> re.Pattern[str]:
|
|
405
|
+
"""Build pattern for matching reference comments.
|
|
406
|
+
|
|
407
|
+
Creates a pattern for single-line comments like:
|
|
408
|
+
- # Implements: REQ-p00001
|
|
409
|
+
- // Validates: REQ-p00002, REQ-p00003
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
pattern_config: Configuration for ID structure
|
|
413
|
+
ref_config: Configuration for comment styles and keywords
|
|
414
|
+
keyword_type: Type of keyword to match ("implements", "validates", "refines")
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Compiled regex pattern for matching reference comments
|
|
418
|
+
"""
|
|
419
|
+
# Build comment prefix pattern
|
|
420
|
+
comment_pattern = _build_comment_prefix_pattern(ref_config.comment_styles)
|
|
421
|
+
|
|
422
|
+
# Get keywords for the type
|
|
423
|
+
keywords = ref_config.keywords.get(keyword_type, [keyword_type.capitalize()])
|
|
424
|
+
keyword_pattern = "|".join(re.escape(k) for k in keywords)
|
|
425
|
+
|
|
426
|
+
# Build ID pattern (simplified for comment matching - captures multiple)
|
|
427
|
+
prefix = pattern_config.prefix
|
|
428
|
+
sep_pattern = _build_separator_pattern(ref_config.separators)
|
|
429
|
+
|
|
430
|
+
# Pattern for a single ID (may include assertion)
|
|
431
|
+
single_id = (
|
|
432
|
+
rf"{re.escape(prefix)}{sep_pattern}[A-Za-z0-9{re.escape(''.join(ref_config.separators))}]+"
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
# Full pattern: comment marker + keyword: + refs
|
|
436
|
+
full_pattern = (
|
|
437
|
+
rf"{comment_pattern}\s*"
|
|
438
|
+
rf"(?:{keyword_pattern}):\s*"
|
|
439
|
+
rf"(?P<refs>{single_id}(?:\s*,\s*{single_id})*)"
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
flags = 0 if ref_config.case_sensitive else re.IGNORECASE
|
|
443
|
+
return re.compile(full_pattern, flags)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def build_block_header_pattern(
|
|
447
|
+
ref_config: ReferenceConfig,
|
|
448
|
+
keyword_type: str = "implements",
|
|
449
|
+
) -> re.Pattern[str]:
|
|
450
|
+
"""Build pattern for multi-line block headers.
|
|
451
|
+
|
|
452
|
+
Creates a pattern for block-style references like:
|
|
453
|
+
- # IMPLEMENTS REQUIREMENTS:
|
|
454
|
+
- // TESTS REQUIREMENTS:
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
ref_config: Configuration for comment styles and keywords
|
|
458
|
+
keyword_type: Type of keyword to match
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
Compiled regex pattern for matching block headers
|
|
462
|
+
"""
|
|
463
|
+
# Build comment prefix pattern
|
|
464
|
+
comment_pattern = _build_comment_prefix_pattern(ref_config.comment_styles)
|
|
465
|
+
|
|
466
|
+
# Get keywords and make variations
|
|
467
|
+
keywords = ref_config.keywords.get(keyword_type, [keyword_type.capitalize()])
|
|
468
|
+
|
|
469
|
+
# Add uppercase versions if not already present
|
|
470
|
+
all_keywords = set(keywords)
|
|
471
|
+
for k in keywords:
|
|
472
|
+
all_keywords.add(k.upper())
|
|
473
|
+
|
|
474
|
+
keyword_pattern = "|".join(re.escape(k) for k in all_keywords)
|
|
475
|
+
|
|
476
|
+
# Block header pattern
|
|
477
|
+
full_pattern = rf"{comment_pattern}\s*(?:{keyword_pattern})\s+REQUIREMENTS?:?\s*$"
|
|
478
|
+
|
|
479
|
+
return re.compile(full_pattern, re.IGNORECASE)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def build_block_ref_pattern(
|
|
483
|
+
pattern_config: PatternConfig,
|
|
484
|
+
ref_config: ReferenceConfig,
|
|
485
|
+
) -> re.Pattern[str]:
|
|
486
|
+
"""Build pattern for references within a block.
|
|
487
|
+
|
|
488
|
+
Creates a pattern for individual refs in a block like:
|
|
489
|
+
- # REQ-p00001
|
|
490
|
+
- // REQ-p00002-A
|
|
491
|
+
|
|
492
|
+
Args:
|
|
493
|
+
pattern_config: Configuration for ID structure
|
|
494
|
+
ref_config: Configuration for comment styles
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
Compiled regex pattern for matching block references
|
|
498
|
+
"""
|
|
499
|
+
# Build comment prefix pattern
|
|
500
|
+
comment_pattern = _build_comment_prefix_pattern(ref_config.comment_styles)
|
|
501
|
+
|
|
502
|
+
# Build ID pattern
|
|
503
|
+
prefix = pattern_config.prefix
|
|
504
|
+
sep_pattern = _build_separator_pattern(ref_config.separators)
|
|
505
|
+
|
|
506
|
+
# Pattern for ID (may include assertion)
|
|
507
|
+
id_pattern = (
|
|
508
|
+
rf"{re.escape(prefix)}{sep_pattern}[A-Za-z0-9{re.escape(''.join(ref_config.separators))}]+"
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
full_pattern = rf"^\s*{comment_pattern}\s+(?P<ref>{id_pattern})"
|
|
512
|
+
|
|
513
|
+
flags = 0 if ref_config.case_sensitive else re.IGNORECASE
|
|
514
|
+
return re.compile(full_pattern, flags)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def extract_ids_from_text(
|
|
518
|
+
text: str,
|
|
519
|
+
pattern_config: PatternConfig,
|
|
520
|
+
ref_config: ReferenceConfig,
|
|
521
|
+
) -> list[str]:
|
|
522
|
+
"""Extract all requirement/assertion IDs from text.
|
|
523
|
+
|
|
524
|
+
Finds all IDs matching the configured pattern in the given text.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
text: Text to search for IDs
|
|
528
|
+
pattern_config: Configuration for ID structure
|
|
529
|
+
ref_config: Configuration for reference matching
|
|
530
|
+
|
|
531
|
+
Returns:
|
|
532
|
+
List of extracted ID strings (normalized)
|
|
533
|
+
"""
|
|
534
|
+
pattern = build_id_pattern(pattern_config, ref_config, include_assertion=True)
|
|
535
|
+
|
|
536
|
+
ids = []
|
|
537
|
+
for match in pattern.finditer(text):
|
|
538
|
+
match.group("full_id")
|
|
539
|
+
normalized = normalize_extracted_id(match, pattern_config, ref_config)
|
|
540
|
+
if normalized:
|
|
541
|
+
ids.append(normalized)
|
|
542
|
+
|
|
543
|
+
return ids
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def normalize_extracted_id(
|
|
547
|
+
match: re.Match[str],
|
|
548
|
+
pattern_config: PatternConfig,
|
|
549
|
+
ref_config: ReferenceConfig,
|
|
550
|
+
) -> str:
|
|
551
|
+
"""Normalize extracted ID to canonical format.
|
|
552
|
+
|
|
553
|
+
Converts matched ID to the standard format defined by pattern_config.
|
|
554
|
+
|
|
555
|
+
Args:
|
|
556
|
+
match: Regex match object with named groups
|
|
557
|
+
pattern_config: Configuration for ID structure
|
|
558
|
+
ref_config: Configuration for separators
|
|
559
|
+
|
|
560
|
+
Returns:
|
|
561
|
+
Normalized ID string in canonical format
|
|
562
|
+
"""
|
|
563
|
+
# Get matched components
|
|
564
|
+
try:
|
|
565
|
+
type_code = match.group("type")
|
|
566
|
+
number = match.group("number")
|
|
567
|
+
except (IndexError, AttributeError):
|
|
568
|
+
# If groups don't exist, return the full match
|
|
569
|
+
return match.group(0)
|
|
570
|
+
|
|
571
|
+
# Build canonical ID using pattern_config
|
|
572
|
+
prefix = pattern_config.prefix
|
|
573
|
+
canonical_sep = "-" # Standard separator for canonical format
|
|
574
|
+
|
|
575
|
+
# Base ID
|
|
576
|
+
canonical = f"{prefix}{canonical_sep}{type_code}{number}"
|
|
577
|
+
|
|
578
|
+
# Add assertion if present
|
|
579
|
+
try:
|
|
580
|
+
assertion = match.group("assertion")
|
|
581
|
+
if assertion:
|
|
582
|
+
canonical = f"{canonical}{canonical_sep}{assertion.upper()}"
|
|
583
|
+
except (IndexError, AttributeError):
|
|
584
|
+
pass
|
|
585
|
+
|
|
586
|
+
return canonical
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
# =============================================================================
|
|
590
|
+
# Helper Functions
|
|
591
|
+
# =============================================================================
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _build_separator_pattern(separators: list[str]) -> str:
|
|
595
|
+
"""Build regex pattern for matching any of the given separators.
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
separators: List of separator characters
|
|
599
|
+
|
|
600
|
+
Returns:
|
|
601
|
+
Regex pattern matching any separator
|
|
602
|
+
"""
|
|
603
|
+
if not separators:
|
|
604
|
+
return "-"
|
|
605
|
+
if len(separators) == 1:
|
|
606
|
+
return re.escape(separators[0])
|
|
607
|
+
return f"[{''.join(re.escape(s) for s in separators)}]"
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def _build_comment_prefix_pattern(comment_styles: list[str]) -> str:
|
|
611
|
+
"""Build regex pattern for matching comment prefixes.
|
|
612
|
+
|
|
613
|
+
Args:
|
|
614
|
+
comment_styles: List of comment style markers
|
|
615
|
+
|
|
616
|
+
Returns:
|
|
617
|
+
Regex pattern matching any comment prefix
|
|
618
|
+
"""
|
|
619
|
+
if not comment_styles:
|
|
620
|
+
return r"(?:#|//|--)"
|
|
621
|
+
|
|
622
|
+
patterns = []
|
|
623
|
+
for style in comment_styles:
|
|
624
|
+
patterns.append(re.escape(style))
|
|
625
|
+
|
|
626
|
+
return f"(?:{'|'.join(patterns)})"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Validation module - Requirement format and content validation.
|
|
2
|
+
|
|
3
|
+
Provides validators for checking requirement format compliance
|
|
4
|
+
based on configurable rules.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from elspais.validation.format import (
|
|
8
|
+
FormatRulesConfig,
|
|
9
|
+
FormatViolation,
|
|
10
|
+
get_format_rules_config,
|
|
11
|
+
validate_requirement_format,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"FormatRulesConfig",
|
|
16
|
+
"FormatViolation",
|
|
17
|
+
"validate_requirement_format",
|
|
18
|
+
"get_format_rules_config",
|
|
19
|
+
]
|