elspais 0.9.1__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 +36 -0
- elspais/__main__.py +8 -0
- elspais/cli.py +525 -0
- elspais/commands/__init__.py +12 -0
- elspais/commands/analyze.py +218 -0
- elspais/commands/config_cmd.py +501 -0
- elspais/commands/edit.py +522 -0
- elspais/commands/hash_cmd.py +174 -0
- elspais/commands/index.py +166 -0
- elspais/commands/init.py +177 -0
- elspais/commands/rules_cmd.py +120 -0
- elspais/commands/trace.py +208 -0
- elspais/commands/validate.py +388 -0
- elspais/config/__init__.py +13 -0
- elspais/config/defaults.py +173 -0
- elspais/config/loader.py +494 -0
- elspais/core/__init__.py +21 -0
- elspais/core/content_rules.py +170 -0
- elspais/core/hasher.py +143 -0
- elspais/core/models.py +318 -0
- elspais/core/parser.py +596 -0
- elspais/core/patterns.py +390 -0
- elspais/core/rules.py +514 -0
- elspais/mcp/__init__.py +42 -0
- elspais/mcp/__main__.py +6 -0
- elspais/mcp/context.py +171 -0
- elspais/mcp/serializers.py +112 -0
- elspais/mcp/server.py +339 -0
- elspais/testing/__init__.py +27 -0
- elspais/testing/config.py +48 -0
- elspais/testing/mapper.py +163 -0
- elspais/testing/result_parser.py +289 -0
- elspais/testing/scanner.py +206 -0
- elspais-0.9.1.dist-info/METADATA +393 -0
- elspais-0.9.1.dist-info/RECORD +38 -0
- elspais-0.9.1.dist-info/WHEEL +4 -0
- elspais-0.9.1.dist-info/entry_points.txt +2 -0
- elspais-0.9.1.dist-info/licenses/LICENSE +21 -0
elspais/core/rules.py
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
"""
|
|
2
|
+
elspais.core.rules - Validation rule engine.
|
|
3
|
+
|
|
4
|
+
Provides configurable validation rules for requirement hierarchies,
|
|
5
|
+
format compliance, and traceability.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any, Dict, List, Optional, Set
|
|
11
|
+
|
|
12
|
+
from elspais.core.models import Requirement
|
|
13
|
+
from elspais.core.patterns import PatternConfig, PatternValidator
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Severity(Enum):
|
|
17
|
+
"""Severity level for rule violations."""
|
|
18
|
+
|
|
19
|
+
ERROR = "error"
|
|
20
|
+
WARNING = "warning"
|
|
21
|
+
INFO = "info"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class RuleViolation:
|
|
26
|
+
"""
|
|
27
|
+
Represents a rule violation found during validation.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
rule_name: Name of the violated rule (e.g., "hierarchy.circular")
|
|
31
|
+
requirement_id: ID of the requirement with the violation
|
|
32
|
+
message: Human-readable description of the violation
|
|
33
|
+
severity: Severity level
|
|
34
|
+
location: File:line location string
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
rule_name: str
|
|
38
|
+
requirement_id: str
|
|
39
|
+
message: str
|
|
40
|
+
severity: Severity
|
|
41
|
+
location: str = ""
|
|
42
|
+
|
|
43
|
+
def __str__(self) -> str:
|
|
44
|
+
prefix = {
|
|
45
|
+
Severity.ERROR: "❌ ERROR",
|
|
46
|
+
Severity.WARNING: "⚠️ WARNING",
|
|
47
|
+
Severity.INFO: "ℹ️ INFO",
|
|
48
|
+
}.get(self.severity, "?")
|
|
49
|
+
return (
|
|
50
|
+
f"{prefix} [{self.rule_name}] {self.requirement_id}\n"
|
|
51
|
+
f" {self.message}\n {self.location}"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class HierarchyConfig:
|
|
57
|
+
"""Configuration for hierarchy validation rules."""
|
|
58
|
+
|
|
59
|
+
allowed_implements: List[str] = field(default_factory=list)
|
|
60
|
+
allow_circular: bool = False
|
|
61
|
+
allow_orphans: bool = False
|
|
62
|
+
max_depth: int = 5
|
|
63
|
+
cross_repo_implements: bool = True
|
|
64
|
+
|
|
65
|
+
# Parsed allowed relationships: source_type -> set of allowed target types
|
|
66
|
+
_allowed_map: Dict[str, Set[str]] = field(default_factory=dict, repr=False)
|
|
67
|
+
|
|
68
|
+
def __post_init__(self) -> None:
|
|
69
|
+
"""Parse allowed_implements into a lookup map."""
|
|
70
|
+
self._allowed_map = {}
|
|
71
|
+
for rule in self.allowed_implements:
|
|
72
|
+
# Parse "dev -> ops, prd"
|
|
73
|
+
parts = rule.split("->")
|
|
74
|
+
if len(parts) == 2:
|
|
75
|
+
source = parts[0].strip().lower()
|
|
76
|
+
targets = [t.strip().lower() for t in parts[1].split(",")]
|
|
77
|
+
self._allowed_map[source] = set(targets)
|
|
78
|
+
|
|
79
|
+
def can_implement(self, source_type: str, target_type: str) -> bool:
|
|
80
|
+
"""Check if source type can implement target type."""
|
|
81
|
+
source = source_type.lower()
|
|
82
|
+
target = target_type.lower()
|
|
83
|
+
allowed = self._allowed_map.get(source, set())
|
|
84
|
+
return target in allowed
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class FormatConfig:
|
|
89
|
+
"""Configuration for format validation rules."""
|
|
90
|
+
|
|
91
|
+
require_hash: bool = True
|
|
92
|
+
require_rationale: bool = False
|
|
93
|
+
require_status: bool = True
|
|
94
|
+
allowed_statuses: List[str] = field(
|
|
95
|
+
default_factory=lambda: ["Active", "Draft", "Deprecated", "Superseded"]
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Assertion format rules
|
|
99
|
+
require_assertions: bool = True
|
|
100
|
+
acceptance_criteria: str = "warn" # "allow" | "warn" | "error"
|
|
101
|
+
require_shall: bool = True
|
|
102
|
+
labels_sequential: bool = True
|
|
103
|
+
labels_unique: bool = True
|
|
104
|
+
placeholder_values: List[str] = field(default_factory=lambda: [
|
|
105
|
+
"obsolete", "removed", "deprecated", "N/A", "n/a", "-", "reserved"
|
|
106
|
+
])
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class RulesConfig:
|
|
111
|
+
"""Complete configuration for all validation rules."""
|
|
112
|
+
|
|
113
|
+
hierarchy: HierarchyConfig = field(default_factory=HierarchyConfig)
|
|
114
|
+
format: FormatConfig = field(default_factory=FormatConfig)
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def from_dict(cls, data: Dict[str, Any]) -> "RulesConfig":
|
|
118
|
+
"""Create RulesConfig from configuration dictionary."""
|
|
119
|
+
hierarchy_data = data.get("hierarchy", {})
|
|
120
|
+
format_data = data.get("format", {})
|
|
121
|
+
|
|
122
|
+
hierarchy = HierarchyConfig(
|
|
123
|
+
allowed_implements=hierarchy_data.get(
|
|
124
|
+
"allowed_implements", ["dev -> ops, prd", "ops -> prd", "prd -> prd"]
|
|
125
|
+
),
|
|
126
|
+
allow_circular=hierarchy_data.get("allow_circular", False),
|
|
127
|
+
allow_orphans=hierarchy_data.get("allow_orphans", False),
|
|
128
|
+
max_depth=hierarchy_data.get("max_depth", 5),
|
|
129
|
+
cross_repo_implements=hierarchy_data.get("cross_repo_implements", True),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
format_config = FormatConfig(
|
|
133
|
+
require_hash=format_data.get("require_hash", True),
|
|
134
|
+
require_rationale=format_data.get("require_rationale", False),
|
|
135
|
+
require_status=format_data.get("require_status", True),
|
|
136
|
+
allowed_statuses=format_data.get(
|
|
137
|
+
"allowed_statuses", ["Active", "Draft", "Deprecated", "Superseded"]
|
|
138
|
+
),
|
|
139
|
+
# Assertion rules
|
|
140
|
+
require_assertions=format_data.get("require_assertions", True),
|
|
141
|
+
acceptance_criteria=format_data.get("acceptance_criteria", "warn"),
|
|
142
|
+
require_shall=format_data.get("require_shall", True),
|
|
143
|
+
labels_sequential=format_data.get("labels_sequential", True),
|
|
144
|
+
labels_unique=format_data.get("labels_unique", True),
|
|
145
|
+
placeholder_values=format_data.get("placeholder_values", [
|
|
146
|
+
"obsolete", "removed", "deprecated", "N/A", "n/a", "-", "reserved"
|
|
147
|
+
]),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return cls(hierarchy=hierarchy, format=format_config)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class RuleEngine:
|
|
154
|
+
"""
|
|
155
|
+
Validates requirements against configured rules.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
def __init__(
|
|
159
|
+
self,
|
|
160
|
+
config: RulesConfig,
|
|
161
|
+
pattern_config: Optional[PatternConfig] = None,
|
|
162
|
+
):
|
|
163
|
+
"""
|
|
164
|
+
Initialize rule engine.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
config: Rules configuration
|
|
168
|
+
pattern_config: Optional pattern configuration for assertion label validation
|
|
169
|
+
"""
|
|
170
|
+
self.config = config
|
|
171
|
+
self.pattern_config = pattern_config
|
|
172
|
+
self.pattern_validator = (
|
|
173
|
+
PatternValidator(pattern_config) if pattern_config else None
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def validate(self, requirements: Dict[str, Requirement]) -> List[RuleViolation]:
|
|
177
|
+
"""
|
|
178
|
+
Validate all requirements against configured rules.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
requirements: Dictionary of requirement ID -> Requirement
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
List of RuleViolation objects
|
|
185
|
+
"""
|
|
186
|
+
violations = []
|
|
187
|
+
|
|
188
|
+
# Run all validation rules
|
|
189
|
+
violations.extend(self._check_hierarchy(requirements))
|
|
190
|
+
violations.extend(self._check_format(requirements))
|
|
191
|
+
violations.extend(self._check_circular(requirements))
|
|
192
|
+
violations.extend(self._check_orphans(requirements))
|
|
193
|
+
|
|
194
|
+
return violations
|
|
195
|
+
|
|
196
|
+
def _check_hierarchy(self, requirements: Dict[str, Requirement]) -> List[RuleViolation]:
|
|
197
|
+
"""Check hierarchy rules (allowed implements)."""
|
|
198
|
+
violations = []
|
|
199
|
+
|
|
200
|
+
for req_id, req in requirements.items():
|
|
201
|
+
source_type = self._get_type_from_level(req.level)
|
|
202
|
+
|
|
203
|
+
for impl_id in req.implements:
|
|
204
|
+
# Find the target requirement
|
|
205
|
+
target_req = self._find_requirement(impl_id, requirements)
|
|
206
|
+
if target_req is None:
|
|
207
|
+
# Target not found - this is a broken link, not hierarchy violation
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
target_type = self._get_type_from_level(target_req.level)
|
|
211
|
+
|
|
212
|
+
# Check if this relationship is allowed
|
|
213
|
+
if not self.config.hierarchy.can_implement(source_type, target_type):
|
|
214
|
+
msg = (
|
|
215
|
+
f"{source_type.upper()} cannot implement "
|
|
216
|
+
f"{target_type.upper()} ({impl_id})"
|
|
217
|
+
)
|
|
218
|
+
violations.append(
|
|
219
|
+
RuleViolation(
|
|
220
|
+
rule_name="hierarchy.implements",
|
|
221
|
+
requirement_id=req_id,
|
|
222
|
+
message=msg,
|
|
223
|
+
severity=Severity.ERROR,
|
|
224
|
+
location=req.location(),
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
return violations
|
|
229
|
+
|
|
230
|
+
def _check_circular(self, requirements: Dict[str, Requirement]) -> List[RuleViolation]:
|
|
231
|
+
"""Check for circular dependencies."""
|
|
232
|
+
if self.config.hierarchy.allow_circular:
|
|
233
|
+
return []
|
|
234
|
+
|
|
235
|
+
violations: List[RuleViolation] = []
|
|
236
|
+
visited: Set[str] = set()
|
|
237
|
+
path: List[str] = []
|
|
238
|
+
|
|
239
|
+
def dfs(req_id: str) -> Optional[List[str]]:
|
|
240
|
+
"""Depth-first search for cycles."""
|
|
241
|
+
if req_id in path:
|
|
242
|
+
# Found a cycle
|
|
243
|
+
cycle_start = path.index(req_id)
|
|
244
|
+
return path[cycle_start:] + [req_id]
|
|
245
|
+
|
|
246
|
+
if req_id in visited:
|
|
247
|
+
return None
|
|
248
|
+
|
|
249
|
+
visited.add(req_id)
|
|
250
|
+
path.append(req_id)
|
|
251
|
+
|
|
252
|
+
req = requirements.get(req_id)
|
|
253
|
+
if req:
|
|
254
|
+
for impl_id in req.implements:
|
|
255
|
+
# Resolve to full ID if needed
|
|
256
|
+
full_id = self._resolve_id(impl_id, requirements)
|
|
257
|
+
if full_id and full_id in requirements:
|
|
258
|
+
cycle = dfs(full_id)
|
|
259
|
+
if cycle:
|
|
260
|
+
return cycle
|
|
261
|
+
|
|
262
|
+
path.pop()
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
# Check each requirement for cycles
|
|
266
|
+
for req_id in requirements:
|
|
267
|
+
visited.clear()
|
|
268
|
+
path.clear()
|
|
269
|
+
cycle = dfs(req_id)
|
|
270
|
+
if cycle:
|
|
271
|
+
cycle_str = " -> ".join(cycle)
|
|
272
|
+
violations.append(
|
|
273
|
+
RuleViolation(
|
|
274
|
+
rule_name="hierarchy.circular",
|
|
275
|
+
requirement_id=req_id,
|
|
276
|
+
message=f"Circular dependency detected: {cycle_str}",
|
|
277
|
+
severity=Severity.ERROR,
|
|
278
|
+
location=requirements[req_id].location(),
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
break # Report only first cycle found
|
|
282
|
+
|
|
283
|
+
return violations
|
|
284
|
+
|
|
285
|
+
def _check_orphans(self, requirements: Dict[str, Requirement]) -> List[RuleViolation]:
|
|
286
|
+
"""Check for orphaned requirements (DEV/OPS without implements)."""
|
|
287
|
+
if self.config.hierarchy.allow_orphans:
|
|
288
|
+
return []
|
|
289
|
+
|
|
290
|
+
violations = []
|
|
291
|
+
|
|
292
|
+
for req_id, req in requirements.items():
|
|
293
|
+
# Skip root level (PRD)
|
|
294
|
+
if req.level.upper() in ["PRD", "PRODUCT"]:
|
|
295
|
+
continue
|
|
296
|
+
|
|
297
|
+
# DEV/OPS should implement something
|
|
298
|
+
if not req.implements:
|
|
299
|
+
violations.append(
|
|
300
|
+
RuleViolation(
|
|
301
|
+
rule_name="hierarchy.orphan",
|
|
302
|
+
requirement_id=req_id,
|
|
303
|
+
message=f"{req.level} requirement has no Implements reference",
|
|
304
|
+
severity=Severity.WARNING,
|
|
305
|
+
location=req.location(),
|
|
306
|
+
)
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
return violations
|
|
310
|
+
|
|
311
|
+
def _check_format(self, requirements: Dict[str, Requirement]) -> List[RuleViolation]:
|
|
312
|
+
"""Check format rules (hash, rationale, assertions, acceptance criteria)."""
|
|
313
|
+
violations = []
|
|
314
|
+
|
|
315
|
+
for req_id, req in requirements.items():
|
|
316
|
+
# Check hash
|
|
317
|
+
if self.config.format.require_hash and not req.hash:
|
|
318
|
+
violations.append(
|
|
319
|
+
RuleViolation(
|
|
320
|
+
rule_name="format.require_hash",
|
|
321
|
+
requirement_id=req_id,
|
|
322
|
+
message="Missing hash footer",
|
|
323
|
+
severity=Severity.ERROR,
|
|
324
|
+
location=req.location(),
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Check rationale
|
|
329
|
+
if self.config.format.require_rationale and not req.rationale:
|
|
330
|
+
violations.append(
|
|
331
|
+
RuleViolation(
|
|
332
|
+
rule_name="format.require_rationale",
|
|
333
|
+
requirement_id=req_id,
|
|
334
|
+
message="Missing Rationale section",
|
|
335
|
+
severity=Severity.WARNING,
|
|
336
|
+
location=req.location(),
|
|
337
|
+
)
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# Check assertions (new format)
|
|
341
|
+
violations.extend(self._check_assertions(req_id, req))
|
|
342
|
+
|
|
343
|
+
# Check acceptance criteria (legacy format)
|
|
344
|
+
acceptance_mode = self.config.format.acceptance_criteria
|
|
345
|
+
if req.acceptance_criteria:
|
|
346
|
+
if acceptance_mode == "error":
|
|
347
|
+
violations.append(
|
|
348
|
+
RuleViolation(
|
|
349
|
+
rule_name="format.acceptance_criteria",
|
|
350
|
+
requirement_id=req_id,
|
|
351
|
+
message="Acceptance Criteria not allowed; use Assertions",
|
|
352
|
+
severity=Severity.ERROR,
|
|
353
|
+
location=req.location(),
|
|
354
|
+
)
|
|
355
|
+
)
|
|
356
|
+
elif acceptance_mode == "warn":
|
|
357
|
+
violations.append(
|
|
358
|
+
RuleViolation(
|
|
359
|
+
rule_name="format.acceptance_criteria",
|
|
360
|
+
requirement_id=req_id,
|
|
361
|
+
message="Acceptance Criteria deprecated; use Assertions",
|
|
362
|
+
severity=Severity.WARNING,
|
|
363
|
+
location=req.location(),
|
|
364
|
+
)
|
|
365
|
+
)
|
|
366
|
+
# "allow" mode: no violation
|
|
367
|
+
|
|
368
|
+
# Check status
|
|
369
|
+
if self.config.format.require_status:
|
|
370
|
+
if req.status not in self.config.format.allowed_statuses:
|
|
371
|
+
allowed = self.config.format.allowed_statuses
|
|
372
|
+
violations.append(
|
|
373
|
+
RuleViolation(
|
|
374
|
+
rule_name="format.status_valid",
|
|
375
|
+
requirement_id=req_id,
|
|
376
|
+
message=f"Invalid status '{req.status}'. Allowed: {allowed}",
|
|
377
|
+
severity=Severity.ERROR,
|
|
378
|
+
location=req.location(),
|
|
379
|
+
)
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
return violations
|
|
383
|
+
|
|
384
|
+
def _check_assertions(
|
|
385
|
+
self, req_id: str, req: Requirement
|
|
386
|
+
) -> List[RuleViolation]:
|
|
387
|
+
"""Check assertion-specific validation rules."""
|
|
388
|
+
violations = []
|
|
389
|
+
|
|
390
|
+
# Check if assertions are required
|
|
391
|
+
if self.config.format.require_assertions and not req.assertions:
|
|
392
|
+
violations.append(
|
|
393
|
+
RuleViolation(
|
|
394
|
+
rule_name="format.require_assertions",
|
|
395
|
+
requirement_id=req_id,
|
|
396
|
+
message="Missing Assertions section",
|
|
397
|
+
severity=Severity.ERROR,
|
|
398
|
+
location=req.location(),
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
return violations # No point checking other assertion rules
|
|
402
|
+
|
|
403
|
+
if not req.assertions:
|
|
404
|
+
return violations
|
|
405
|
+
|
|
406
|
+
# Extract labels and check for duplicates
|
|
407
|
+
labels = [a.label for a in req.assertions]
|
|
408
|
+
|
|
409
|
+
# Check labels are unique
|
|
410
|
+
if self.config.format.labels_unique:
|
|
411
|
+
seen = set()
|
|
412
|
+
for label in labels:
|
|
413
|
+
if label in seen:
|
|
414
|
+
violations.append(
|
|
415
|
+
RuleViolation(
|
|
416
|
+
rule_name="format.labels_unique",
|
|
417
|
+
requirement_id=req_id,
|
|
418
|
+
message=f"Duplicate assertion label: {label}",
|
|
419
|
+
severity=Severity.ERROR,
|
|
420
|
+
location=req.location(),
|
|
421
|
+
)
|
|
422
|
+
)
|
|
423
|
+
seen.add(label)
|
|
424
|
+
|
|
425
|
+
# Check labels are sequential
|
|
426
|
+
if self.config.format.labels_sequential and self.pattern_validator:
|
|
427
|
+
expected_labels = []
|
|
428
|
+
for i in range(len(labels)):
|
|
429
|
+
expected_labels.append(
|
|
430
|
+
self.pattern_validator.format_assertion_label(i)
|
|
431
|
+
)
|
|
432
|
+
if labels != expected_labels:
|
|
433
|
+
msg = f"Labels not sequential: {labels} (expected {expected_labels})"
|
|
434
|
+
violations.append(
|
|
435
|
+
RuleViolation(
|
|
436
|
+
rule_name="format.labels_sequential",
|
|
437
|
+
requirement_id=req_id,
|
|
438
|
+
message=msg,
|
|
439
|
+
severity=Severity.ERROR,
|
|
440
|
+
location=req.location(),
|
|
441
|
+
)
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
# Check SHALL/SHALL NOT language (skip placeholders)
|
|
445
|
+
if self.config.format.require_shall:
|
|
446
|
+
for assertion in req.assertions:
|
|
447
|
+
if assertion.is_placeholder:
|
|
448
|
+
continue
|
|
449
|
+
if "SHALL" not in assertion.text.upper():
|
|
450
|
+
text_preview = assertion.text[:40]
|
|
451
|
+
msg = f"Assertion {assertion.label} missing SHALL: {text_preview}..."
|
|
452
|
+
violations.append(
|
|
453
|
+
RuleViolation(
|
|
454
|
+
rule_name="format.require_shall",
|
|
455
|
+
requirement_id=req_id,
|
|
456
|
+
message=msg,
|
|
457
|
+
severity=Severity.WARNING,
|
|
458
|
+
location=req.location(),
|
|
459
|
+
)
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
# Validate assertion labels against configured pattern
|
|
463
|
+
if self.pattern_validator:
|
|
464
|
+
for assertion in req.assertions:
|
|
465
|
+
if not self.pattern_validator.is_valid_assertion_label(assertion.label):
|
|
466
|
+
violations.append(
|
|
467
|
+
RuleViolation(
|
|
468
|
+
rule_name="format.assertion_label",
|
|
469
|
+
requirement_id=req_id,
|
|
470
|
+
message=f"Invalid assertion label format: {assertion.label}",
|
|
471
|
+
severity=Severity.ERROR,
|
|
472
|
+
location=req.location(),
|
|
473
|
+
)
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
return violations
|
|
477
|
+
|
|
478
|
+
def _get_type_from_level(self, level: str) -> str:
|
|
479
|
+
"""Map level name to type code."""
|
|
480
|
+
level_map = {
|
|
481
|
+
"PRD": "prd",
|
|
482
|
+
"PRODUCT": "prd",
|
|
483
|
+
"OPS": "ops",
|
|
484
|
+
"OPERATIONS": "ops",
|
|
485
|
+
"DEV": "dev",
|
|
486
|
+
"DEVELOPMENT": "dev",
|
|
487
|
+
}
|
|
488
|
+
return level_map.get(level.upper(), level.lower())
|
|
489
|
+
|
|
490
|
+
def _find_requirement(
|
|
491
|
+
self, impl_id: str, requirements: Dict[str, Requirement]
|
|
492
|
+
) -> Optional[Requirement]:
|
|
493
|
+
"""Find a requirement by ID (handles partial IDs)."""
|
|
494
|
+
# Try exact match first
|
|
495
|
+
if impl_id in requirements:
|
|
496
|
+
return requirements[impl_id]
|
|
497
|
+
|
|
498
|
+
# Try to find by suffix (e.g., "p00001" matches "REQ-p00001")
|
|
499
|
+
for req_id, req in requirements.items():
|
|
500
|
+
if req_id.endswith(impl_id) or req_id.endswith(f"-{impl_id}"):
|
|
501
|
+
return req
|
|
502
|
+
|
|
503
|
+
return None
|
|
504
|
+
|
|
505
|
+
def _resolve_id(self, impl_id: str, requirements: Dict[str, Requirement]) -> Optional[str]:
|
|
506
|
+
"""Resolve a partial ID to a full ID."""
|
|
507
|
+
if impl_id in requirements:
|
|
508
|
+
return impl_id
|
|
509
|
+
|
|
510
|
+
for req_id in requirements:
|
|
511
|
+
if req_id.endswith(impl_id) or req_id.endswith(f"-{impl_id}"):
|
|
512
|
+
return req_id
|
|
513
|
+
|
|
514
|
+
return None
|
elspais/mcp/__init__.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
elspais.mcp - MCP (Model Context Protocol) server for elspais.
|
|
3
|
+
|
|
4
|
+
This module provides an MCP server that exposes elspais functionality
|
|
5
|
+
to AI agents and LLMs. Requires the optional 'mcp' dependency:
|
|
6
|
+
|
|
7
|
+
pip install elspais[mcp]
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
elspais mcp serve # Start with stdio transport
|
|
11
|
+
python -m elspais.mcp # Alternative entry point
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from elspais.mcp.context import WorkspaceContext
|
|
15
|
+
from elspais.mcp.serializers import (
|
|
16
|
+
serialize_assertion,
|
|
17
|
+
serialize_content_rule,
|
|
18
|
+
serialize_requirement,
|
|
19
|
+
serialize_requirement_summary,
|
|
20
|
+
serialize_violation,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"WorkspaceContext",
|
|
25
|
+
"serialize_assertion",
|
|
26
|
+
"serialize_content_rule",
|
|
27
|
+
"serialize_requirement",
|
|
28
|
+
"serialize_requirement_summary",
|
|
29
|
+
"serialize_violation",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def create_server(working_dir=None):
|
|
34
|
+
"""Create MCP server instance."""
|
|
35
|
+
from elspais.mcp.server import create_server as _create
|
|
36
|
+
return _create(working_dir)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def run_server(working_dir=None, transport="stdio"):
|
|
40
|
+
"""Run MCP server."""
|
|
41
|
+
from elspais.mcp.server import run_server as _run
|
|
42
|
+
return _run(working_dir, transport)
|