elspais 0.11.1__py3-none-any.whl → 0.43.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. elspais/__init__.py +2 -11
  2. elspais/{sponsors/__init__.py → associates.py} +102 -58
  3. elspais/cli.py +395 -79
  4. elspais/commands/__init__.py +9 -3
  5. elspais/commands/analyze.py +121 -173
  6. elspais/commands/changed.py +15 -30
  7. elspais/commands/config_cmd.py +13 -16
  8. elspais/commands/edit.py +60 -44
  9. elspais/commands/example_cmd.py +319 -0
  10. elspais/commands/hash_cmd.py +167 -183
  11. elspais/commands/health.py +1177 -0
  12. elspais/commands/index.py +98 -114
  13. elspais/commands/init.py +103 -26
  14. elspais/commands/reformat_cmd.py +41 -444
  15. elspais/commands/rules_cmd.py +7 -3
  16. elspais/commands/trace.py +444 -321
  17. elspais/commands/validate.py +195 -415
  18. elspais/config/__init__.py +799 -5
  19. elspais/{core/content_rules.py → content_rules.py} +20 -3
  20. elspais/docs/cli/assertions.md +67 -0
  21. elspais/docs/cli/commands.md +304 -0
  22. elspais/docs/cli/config.md +262 -0
  23. elspais/docs/cli/format.md +66 -0
  24. elspais/docs/cli/git.md +45 -0
  25. elspais/docs/cli/health.md +190 -0
  26. elspais/docs/cli/hierarchy.md +60 -0
  27. elspais/docs/cli/ignore.md +72 -0
  28. elspais/docs/cli/mcp.md +245 -0
  29. elspais/docs/cli/quickstart.md +58 -0
  30. elspais/docs/cli/traceability.md +89 -0
  31. elspais/docs/cli/validation.md +96 -0
  32. elspais/graph/GraphNode.py +383 -0
  33. elspais/graph/__init__.py +40 -0
  34. elspais/graph/annotators.py +927 -0
  35. elspais/graph/builder.py +1886 -0
  36. elspais/graph/deserializer.py +248 -0
  37. elspais/graph/factory.py +284 -0
  38. elspais/graph/metrics.py +127 -0
  39. elspais/graph/mutations.py +161 -0
  40. elspais/graph/parsers/__init__.py +156 -0
  41. elspais/graph/parsers/code.py +213 -0
  42. elspais/graph/parsers/comments.py +112 -0
  43. elspais/graph/parsers/config_helpers.py +29 -0
  44. elspais/graph/parsers/heredocs.py +225 -0
  45. elspais/graph/parsers/journey.py +131 -0
  46. elspais/graph/parsers/remainder.py +79 -0
  47. elspais/graph/parsers/requirement.py +347 -0
  48. elspais/graph/parsers/results/__init__.py +6 -0
  49. elspais/graph/parsers/results/junit_xml.py +229 -0
  50. elspais/graph/parsers/results/pytest_json.py +313 -0
  51. elspais/graph/parsers/test.py +305 -0
  52. elspais/graph/relations.py +78 -0
  53. elspais/graph/serialize.py +216 -0
  54. elspais/html/__init__.py +8 -0
  55. elspais/html/generator.py +731 -0
  56. elspais/html/templates/trace_view.html.j2 +2151 -0
  57. elspais/mcp/__init__.py +47 -29
  58. elspais/mcp/__main__.py +5 -1
  59. elspais/mcp/file_mutations.py +138 -0
  60. elspais/mcp/server.py +2016 -247
  61. elspais/testing/__init__.py +4 -4
  62. elspais/testing/config.py +3 -0
  63. elspais/testing/mapper.py +1 -1
  64. elspais/testing/result_parser.py +25 -21
  65. elspais/testing/scanner.py +301 -12
  66. elspais/utilities/__init__.py +1 -0
  67. elspais/utilities/docs_loader.py +115 -0
  68. elspais/utilities/git.py +607 -0
  69. elspais/{core → utilities}/hasher.py +8 -22
  70. elspais/utilities/md_renderer.py +189 -0
  71. elspais/{core → utilities}/patterns.py +58 -57
  72. elspais/utilities/reference_config.py +626 -0
  73. elspais/validation/__init__.py +19 -0
  74. elspais/validation/format.py +264 -0
  75. {elspais-0.11.1.dist-info → elspais-0.43.5.dist-info}/METADATA +7 -4
  76. elspais-0.43.5.dist-info/RECORD +80 -0
  77. elspais/config/defaults.py +0 -173
  78. elspais/config/loader.py +0 -494
  79. elspais/core/__init__.py +0 -21
  80. elspais/core/git.py +0 -352
  81. elspais/core/models.py +0 -320
  82. elspais/core/parser.py +0 -640
  83. elspais/core/rules.py +0 -514
  84. elspais/mcp/context.py +0 -171
  85. elspais/mcp/serializers.py +0 -112
  86. elspais/reformat/__init__.py +0 -50
  87. elspais/reformat/detector.py +0 -119
  88. elspais/reformat/hierarchy.py +0 -246
  89. elspais/reformat/line_breaks.py +0 -220
  90. elspais/reformat/prompts.py +0 -123
  91. elspais/reformat/transformer.py +0 -264
  92. elspais/trace_view/__init__.py +0 -54
  93. elspais/trace_view/coverage.py +0 -183
  94. elspais/trace_view/generators/__init__.py +0 -12
  95. elspais/trace_view/generators/base.py +0 -329
  96. elspais/trace_view/generators/csv.py +0 -122
  97. elspais/trace_view/generators/markdown.py +0 -175
  98. elspais/trace_view/html/__init__.py +0 -31
  99. elspais/trace_view/html/generator.py +0 -1006
  100. elspais/trace_view/html/templates/base.html +0 -283
  101. elspais/trace_view/html/templates/components/code_viewer_modal.html +0 -14
  102. elspais/trace_view/html/templates/components/file_picker_modal.html +0 -20
  103. elspais/trace_view/html/templates/components/legend_modal.html +0 -69
  104. elspais/trace_view/html/templates/components/review_panel.html +0 -118
  105. elspais/trace_view/html/templates/partials/review/help/help-panel.json +0 -244
  106. elspais/trace_view/html/templates/partials/review/help/onboarding.json +0 -77
  107. elspais/trace_view/html/templates/partials/review/help/tooltips.json +0 -237
  108. elspais/trace_view/html/templates/partials/review/review-comments.js +0 -928
  109. elspais/trace_view/html/templates/partials/review/review-data.js +0 -961
  110. elspais/trace_view/html/templates/partials/review/review-help.js +0 -679
  111. elspais/trace_view/html/templates/partials/review/review-init.js +0 -177
  112. elspais/trace_view/html/templates/partials/review/review-line-numbers.js +0 -429
  113. elspais/trace_view/html/templates/partials/review/review-packages.js +0 -1029
  114. elspais/trace_view/html/templates/partials/review/review-position.js +0 -540
  115. elspais/trace_view/html/templates/partials/review/review-resize.js +0 -115
  116. elspais/trace_view/html/templates/partials/review/review-status.js +0 -659
  117. elspais/trace_view/html/templates/partials/review/review-sync.js +0 -992
  118. elspais/trace_view/html/templates/partials/review-styles.css +0 -2238
  119. elspais/trace_view/html/templates/partials/scripts.js +0 -1741
  120. elspais/trace_view/html/templates/partials/styles.css +0 -1756
  121. elspais/trace_view/models.py +0 -353
  122. elspais/trace_view/review/__init__.py +0 -60
  123. elspais/trace_view/review/branches.py +0 -1149
  124. elspais/trace_view/review/models.py +0 -1205
  125. elspais/trace_view/review/position.py +0 -609
  126. elspais/trace_view/review/server.py +0 -1056
  127. elspais/trace_view/review/status.py +0 -470
  128. elspais/trace_view/review/storage.py +0 -1367
  129. elspais/trace_view/scanning.py +0 -213
  130. elspais/trace_view/specs/README.md +0 -84
  131. elspais/trace_view/specs/tv-d00001-template-architecture.md +0 -36
  132. elspais/trace_view/specs/tv-d00002-css-extraction.md +0 -37
  133. elspais/trace_view/specs/tv-d00003-js-extraction.md +0 -43
  134. elspais/trace_view/specs/tv-d00004-build-embedding.md +0 -40
  135. elspais/trace_view/specs/tv-d00005-test-format.md +0 -78
  136. elspais/trace_view/specs/tv-d00010-review-data-models.md +0 -33
  137. elspais/trace_view/specs/tv-d00011-review-storage.md +0 -33
  138. elspais/trace_view/specs/tv-d00012-position-resolution.md +0 -33
  139. elspais/trace_view/specs/tv-d00013-git-branches.md +0 -31
  140. elspais/trace_view/specs/tv-d00014-review-api-server.md +0 -31
  141. elspais/trace_view/specs/tv-d00015-status-modifier.md +0 -27
  142. elspais/trace_view/specs/tv-d00016-js-integration.md +0 -33
  143. elspais/trace_view/specs/tv-p00001-html-generator.md +0 -33
  144. elspais/trace_view/specs/tv-p00002-review-system.md +0 -29
  145. elspais-0.11.1.dist-info/RECORD +0 -101
  146. {elspais-0.11.1.dist-info → elspais-0.43.5.dist-info}/WHEEL +0 -0
  147. {elspais-0.11.1.dist-info → elspais-0.43.5.dist-info}/entry_points.txt +0 -0
  148. {elspais-0.11.1.dist-info → elspais-0.43.5.dist-info}/licenses/LICENSE +0 -0
elspais/core/rules.py DELETED
@@ -1,514 +0,0 @@
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/context.py DELETED
@@ -1,171 +0,0 @@
1
- """
2
- elspais.mcp.context - Workspace context for MCP server.
3
-
4
- Manages workspace state including configuration, requirements cache,
5
- and content rules.
6
- """
7
-
8
- import re
9
- from dataclasses import dataclass, field
10
- from pathlib import Path
11
- from typing import Any, Dict, List, Optional
12
-
13
- from elspais.config.loader import find_config_file, get_spec_directories, load_config
14
- from elspais.core.content_rules import load_content_rules
15
- from elspais.core.models import ContentRule, Requirement
16
- from elspais.core.parser import RequirementParser
17
- from elspais.core.patterns import PatternConfig
18
-
19
-
20
- @dataclass
21
- class WorkspaceContext:
22
- """
23
- Manages workspace state for MCP server operations.
24
-
25
- Provides caching of parsed requirements and access to configuration,
26
- content rules, and other workspace resources.
27
- """
28
-
29
- working_dir: Path
30
- config: Dict[str, Any] = field(default_factory=dict)
31
- _requirements_cache: Optional[Dict[str, Requirement]] = field(default=None, repr=False)
32
- _parser: Optional[RequirementParser] = field(default=None, repr=False)
33
-
34
- @classmethod
35
- def from_directory(cls, directory: Path) -> "WorkspaceContext":
36
- """
37
- Initialize context from a working directory.
38
-
39
- Loads configuration from .elspais.toml if found.
40
-
41
- Args:
42
- directory: Working directory path
43
-
44
- Returns:
45
- Initialized WorkspaceContext
46
- """
47
- directory = directory.resolve()
48
- config_path = find_config_file(directory)
49
-
50
- if config_path:
51
- config = load_config(config_path)
52
- else:
53
- # Use defaults
54
- from elspais.config.defaults import DEFAULT_CONFIG
55
- config = DEFAULT_CONFIG.copy()
56
-
57
- return cls(working_dir=directory, config=config)
58
-
59
- def get_requirements(self, force_refresh: bool = False) -> Dict[str, Requirement]:
60
- """
61
- Get all parsed requirements, with caching.
62
-
63
- Args:
64
- force_refresh: If True, ignore cache and re-parse
65
-
66
- Returns:
67
- Dict mapping requirement IDs to Requirement objects
68
- """
69
- if self._requirements_cache is None or force_refresh:
70
- self._requirements_cache = self._parse_requirements()
71
- return self._requirements_cache
72
-
73
- def get_requirement(self, req_id: str) -> Optional[Requirement]:
74
- """
75
- Get a single requirement by ID.
76
-
77
- Args:
78
- req_id: Requirement ID (e.g., "REQ-p00001")
79
-
80
- Returns:
81
- Requirement if found, None otherwise
82
- """
83
- requirements = self.get_requirements()
84
- return requirements.get(req_id)
85
-
86
- def get_content_rules(self) -> List[ContentRule]:
87
- """
88
- Get all configured content rules.
89
-
90
- Returns:
91
- List of ContentRule objects
92
- """
93
- return load_content_rules(self.config, self.working_dir)
94
-
95
- def search_requirements(
96
- self,
97
- query: str,
98
- field: str = "all",
99
- regex: bool = False,
100
- ) -> List[Requirement]:
101
- """
102
- Search requirements by pattern.
103
-
104
- Args:
105
- query: Search query string
106
- field: Field to search - "all", "id", "title", "body", "assertions"
107
- regex: If True, treat query as regex pattern
108
-
109
- Returns:
110
- List of matching requirements
111
- """
112
- requirements = self.get_requirements()
113
- results = []
114
-
115
- if regex:
116
- pattern = re.compile(query, re.IGNORECASE)
117
- else:
118
- pattern = re.compile(re.escape(query), re.IGNORECASE)
119
-
120
- for req in requirements.values():
121
- if self._matches(req, pattern, field):
122
- results.append(req)
123
-
124
- return results
125
-
126
- def invalidate_cache(self) -> None:
127
- """Clear cached requirements (call after edits)."""
128
- self._requirements_cache = None
129
-
130
- def _parse_requirements(self) -> Dict[str, Requirement]:
131
- """Parse requirements from spec directories."""
132
- if self._parser is None:
133
- pattern_config = PatternConfig.from_dict(self.config.get("patterns", {}))
134
- self._parser = RequirementParser(pattern_config)
135
-
136
- spec_dirs = get_spec_directories(None, self.config, self.working_dir)
137
- skip_files = self.config.get("spec", {}).get("skip_files", [])
138
-
139
- all_requirements: Dict[str, Requirement] = {}
140
-
141
- for spec_dir in spec_dirs:
142
- if spec_dir.exists():
143
- requirements = self._parser.parse_directory(spec_dir, skip_files=skip_files)
144
- all_requirements.update(requirements)
145
-
146
- return all_requirements
147
-
148
- def _matches(self, req: Requirement, pattern: re.Pattern, field: str) -> bool:
149
- """Check if requirement matches search pattern."""
150
- if field == "id":
151
- return bool(pattern.search(req.id))
152
- elif field == "title":
153
- return bool(pattern.search(req.title))
154
- elif field == "body":
155
- return bool(pattern.search(req.body))
156
- elif field == "assertions":
157
- for assertion in req.assertions:
158
- if pattern.search(assertion.text):
159
- return True
160
- return False
161
- else: # "all"
162
- if pattern.search(req.id):
163
- return True
164
- if pattern.search(req.title):
165
- return True
166
- if pattern.search(req.body):
167
- return True
168
- for assertion in req.assertions:
169
- if pattern.search(assertion.text):
170
- return True
171
- return False