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
@@ -1,112 +0,0 @@
1
- """
2
- elspais.mcp.serializers - JSON serialization for MCP responses.
3
-
4
- Provides functions to serialize elspais data models to JSON-compatible dicts.
5
- """
6
-
7
- from typing import Any, Dict, List
8
-
9
- from elspais.core.models import Assertion, ContentRule, Requirement
10
- from elspais.core.rules import RuleViolation
11
-
12
-
13
- def serialize_requirement(req: Requirement) -> Dict[str, Any]:
14
- """
15
- Serialize a Requirement to a JSON-compatible dict.
16
-
17
- Args:
18
- req: Requirement to serialize
19
-
20
- Returns:
21
- Dict suitable for JSON serialization
22
- """
23
- return {
24
- "id": req.id,
25
- "title": req.title,
26
- "level": req.level,
27
- "status": req.status,
28
- "body": req.body,
29
- "implements": req.implements,
30
- "assertions": [serialize_assertion(a) for a in req.assertions],
31
- "rationale": req.rationale,
32
- "hash": req.hash,
33
- "file_path": str(req.file_path) if req.file_path else None,
34
- "line_number": req.line_number,
35
- "subdir": req.subdir,
36
- "type_code": req.type_code,
37
- }
38
-
39
-
40
- def serialize_requirement_summary(req: Requirement) -> Dict[str, Any]:
41
- """
42
- Serialize requirement summary (lighter weight, for listings).
43
-
44
- Args:
45
- req: Requirement to serialize
46
-
47
- Returns:
48
- Dict with summary fields only
49
- """
50
- return {
51
- "id": req.id,
52
- "title": req.title,
53
- "level": req.level,
54
- "status": req.status,
55
- "implements": req.implements,
56
- "assertion_count": len(req.assertions),
57
- }
58
-
59
-
60
- def serialize_assertion(assertion: Assertion) -> Dict[str, Any]:
61
- """
62
- Serialize an Assertion to a JSON-compatible dict.
63
-
64
- Args:
65
- assertion: Assertion to serialize
66
-
67
- Returns:
68
- Dict suitable for JSON serialization
69
- """
70
- return {
71
- "label": assertion.label,
72
- "text": assertion.text,
73
- "is_placeholder": assertion.is_placeholder,
74
- }
75
-
76
-
77
- def serialize_violation(violation: RuleViolation) -> Dict[str, Any]:
78
- """
79
- Serialize a RuleViolation to a JSON-compatible dict.
80
-
81
- Args:
82
- violation: RuleViolation to serialize
83
-
84
- Returns:
85
- Dict suitable for JSON serialization
86
- """
87
- return {
88
- "rule_name": violation.rule_name,
89
- "requirement_id": violation.requirement_id,
90
- "message": violation.message,
91
- "severity": violation.severity.value,
92
- "location": violation.location,
93
- }
94
-
95
-
96
- def serialize_content_rule(rule: ContentRule) -> Dict[str, Any]:
97
- """
98
- Serialize a ContentRule to a JSON-compatible dict.
99
-
100
- Args:
101
- rule: ContentRule to serialize
102
-
103
- Returns:
104
- Dict suitable for JSON serialization
105
- """
106
- return {
107
- "file_path": str(rule.file_path),
108
- "title": rule.title,
109
- "content": rule.content,
110
- "type": rule.type,
111
- "applies_to": rule.applies_to,
112
- }
@@ -1,50 +0,0 @@
1
- # Implements: REQ-int-d00008 (Reformat Command)
2
- """
3
- elspais.reformat - Requirement format transformation.
4
-
5
- Transforms legacy Acceptance Criteria format to Assertions format.
6
- Also provides line break normalization.
7
-
8
- IMPLEMENTS REQUIREMENTS:
9
- REQ-int-d00008: Reformat Command
10
- """
11
-
12
- from elspais.reformat.detector import detect_format, needs_reformatting, FormatAnalysis
13
- from elspais.reformat.transformer import (
14
- reformat_requirement,
15
- assemble_new_format,
16
- validate_reformatted_content,
17
- )
18
- from elspais.reformat.line_breaks import (
19
- normalize_line_breaks,
20
- fix_requirement_line_breaks,
21
- detect_line_break_issues,
22
- )
23
- from elspais.reformat.hierarchy import (
24
- RequirementNode,
25
- get_all_requirements,
26
- build_hierarchy,
27
- traverse_top_down,
28
- normalize_req_id,
29
- )
30
-
31
- __all__ = [
32
- # Detection
33
- "detect_format",
34
- "needs_reformatting",
35
- "FormatAnalysis",
36
- # Transformation
37
- "reformat_requirement",
38
- "assemble_new_format",
39
- "validate_reformatted_content",
40
- # Line breaks
41
- "normalize_line_breaks",
42
- "fix_requirement_line_breaks",
43
- "detect_line_break_issues",
44
- # Hierarchy
45
- "RequirementNode",
46
- "get_all_requirements",
47
- "build_hierarchy",
48
- "traverse_top_down",
49
- "normalize_req_id",
50
- ]
@@ -1,119 +0,0 @@
1
- # Implements: REQ-int-d00008 (Reformat Command)
2
- """
3
- Format detection for requirements.
4
-
5
- Detects whether a requirement is in old format (needs reformatting)
6
- or new format (already reformatted).
7
- """
8
-
9
- import re
10
- from dataclasses import dataclass
11
-
12
-
13
- @dataclass
14
- class FormatAnalysis:
15
- """Result of format detection analysis."""
16
- is_new_format: bool
17
- has_assertions_section: bool
18
- has_labeled_assertions: bool
19
- has_acceptance_criteria: bool
20
- uses_shall_language: bool
21
- assertion_count: int
22
- confidence: float # 0.0 to 1.0
23
-
24
-
25
- def detect_format(body: str, rationale: str = "") -> FormatAnalysis:
26
- """
27
- Detect whether a requirement is in old or new format.
28
-
29
- New format indicators:
30
- - Has '## Assertions' section with labeled assertions (A., B., C.)
31
- - Does NOT have '**Acceptance Criteria**:' section
32
- - Uses prescriptive SHALL language in assertions
33
-
34
- Old format indicators:
35
- - Has '**Acceptance Criteria**:' or 'Acceptance Criteria:' section
36
- - Uses descriptive language (does, has, provides) without labeled assertions
37
- - May have bullet points without letter labels
38
-
39
- Args:
40
- body: The requirement body text
41
- rationale: Optional rationale text
42
-
43
- Returns:
44
- FormatAnalysis with detection results
45
- """
46
- full_text = f"{body}\n{rationale}".strip()
47
-
48
- # Check for ## Assertions section
49
- has_assertions_section = bool(
50
- re.search(r'^##\s+Assertions\s*$', full_text, re.MULTILINE)
51
- )
52
-
53
- # Check for labeled assertions (A., B., C., etc. followed by SHALL somewhere in the line)
54
- labeled_assertions = re.findall(
55
- r'^[A-Z]\.\s+.*\bSHALL\b',
56
- full_text,
57
- re.MULTILINE | re.IGNORECASE
58
- )
59
- has_labeled_assertions = len(labeled_assertions) >= 1
60
- assertion_count = len(labeled_assertions)
61
-
62
- # Check for Acceptance Criteria section
63
- has_acceptance_criteria = bool(re.search(
64
- r'\*?\*?Acceptance\s+Criteria\*?\*?\s*:',
65
- full_text,
66
- re.IGNORECASE
67
- ))
68
-
69
- # Check for SHALL language usage anywhere
70
- shall_count = len(re.findall(r'\bSHALL\b', full_text, re.IGNORECASE))
71
- uses_shall_language = shall_count >= 1
72
-
73
- # Determine if new format
74
- # New format: has Assertions section with labeled assertions, no Acceptance Criteria
75
- is_new_format = (
76
- has_assertions_section and
77
- has_labeled_assertions and
78
- not has_acceptance_criteria
79
- )
80
-
81
- # Calculate confidence score
82
- confidence = 0.0
83
- if has_assertions_section:
84
- confidence += 0.35
85
- if has_labeled_assertions:
86
- confidence += 0.35
87
- if not has_acceptance_criteria:
88
- confidence += 0.20
89
- if uses_shall_language:
90
- confidence += 0.10
91
-
92
- # Invert confidence if old format
93
- if not is_new_format:
94
- confidence = 1.0 - confidence
95
-
96
- return FormatAnalysis(
97
- is_new_format=is_new_format,
98
- has_assertions_section=has_assertions_section,
99
- has_labeled_assertions=has_labeled_assertions,
100
- has_acceptance_criteria=has_acceptance_criteria,
101
- uses_shall_language=uses_shall_language,
102
- assertion_count=assertion_count,
103
- confidence=confidence
104
- )
105
-
106
-
107
- def needs_reformatting(body: str, rationale: str = "") -> bool:
108
- """
109
- Simple check if a requirement needs reformatting.
110
-
111
- Args:
112
- body: The requirement body text
113
- rationale: Optional rationale text
114
-
115
- Returns:
116
- True if the requirement needs reformatting (is in old format)
117
- """
118
- analysis = detect_format(body, rationale)
119
- return not analysis.is_new_format
@@ -1,246 +0,0 @@
1
- # Implements: REQ-int-d00008 (Reformat Command)
2
- """
3
- Hierarchy traversal logic for requirements.
4
-
5
- Uses elspais core modules directly to parse requirements and build
6
- a traversable hierarchy based on implements relationships.
7
- """
8
-
9
- import sys
10
- from dataclasses import dataclass, field
11
- from pathlib import Path
12
- from typing import Callable, Dict, List, Optional, TYPE_CHECKING
13
-
14
- if TYPE_CHECKING:
15
- from elspais.core.models import Requirement
16
- from elspais.core.patterns import PatternValidator
17
-
18
-
19
- @dataclass
20
- class RequirementNode:
21
- """Represents a requirement with its metadata and hierarchy info."""
22
- req_id: str
23
- title: str
24
- body: str
25
- rationale: str
26
- file_path: str
27
- line: int
28
- implements: List[str] # Parent REQ IDs
29
- hash: str
30
- status: str
31
- level: str
32
- children: List[str] = field(default_factory=list) # Child REQ IDs
33
-
34
- @classmethod
35
- def from_core(cls, req: "Requirement") -> "RequirementNode":
36
- """
37
- Create a RequirementNode from a core Requirement object.
38
-
39
- Args:
40
- req: Core Requirement object from elspais.core.models
41
-
42
- Returns:
43
- RequirementNode with mapped fields
44
- """
45
- return cls(
46
- req_id=req.id,
47
- title=req.title,
48
- body=req.body,
49
- rationale=req.rationale or "",
50
- file_path=str(req.file_path) if req.file_path else "",
51
- line=req.line_number or 0,
52
- implements=list(req.implements),
53
- hash=req.hash or "",
54
- status=req.status,
55
- level=req.level,
56
- children=[],
57
- )
58
-
59
-
60
- def get_all_requirements(
61
- config_path: Optional[Path] = None,
62
- base_path: Optional[Path] = None,
63
- mode: str = "combined",
64
- ) -> Dict[str, RequirementNode]:
65
- """
66
- Get all requirements using core parser directly.
67
-
68
- Args:
69
- config_path: Optional path to .elspais.toml config file
70
- base_path: Base path for resolving relative directories
71
- mode: Which repos to include:
72
- - "combined" (default): Load local + core/associated repo requirements
73
- - "core-only": Load only core/associated repo requirements
74
- - "local-only": Load only local requirements
75
-
76
- Returns:
77
- Dict mapping requirement ID (e.g., 'REQ-d00027') to RequirementNode
78
- """
79
- from elspais.config.loader import load_config, find_config_file, get_spec_directories
80
- from elspais.core.parser import RequirementParser
81
- from elspais.core.patterns import PatternConfig
82
- from elspais.commands.validate import load_requirements_from_repo
83
-
84
- # Find and load config
85
- if config_path is None:
86
- config_path = find_config_file(base_path or Path.cwd())
87
-
88
- if config_path is None:
89
- print("Warning: No .elspais.toml found", file=sys.stderr)
90
- return {}
91
-
92
- try:
93
- config = load_config(config_path)
94
- except Exception as e:
95
- print(f"Warning: Failed to load config: {e}", file=sys.stderr)
96
- return {}
97
-
98
- requirements = {}
99
-
100
- # Load local requirements (unless core-only mode)
101
- if mode in ("combined", "local-only"):
102
- # Create parser with pattern config
103
- pattern_config = PatternConfig.from_dict(config.get("patterns", {}))
104
- parser = RequirementParser(pattern_config)
105
-
106
- # Get spec directories
107
- spec_dirs = get_spec_directories(None, config, base_path or config_path.parent)
108
-
109
- if spec_dirs:
110
- try:
111
- parse_result = parser.parse_directories(spec_dirs)
112
- for req_id, req in parse_result.requirements.items():
113
- requirements[req_id] = RequirementNode.from_core(req)
114
- except Exception as e:
115
- print(f"Warning: Failed to parse local requirements: {e}", file=sys.stderr)
116
-
117
- # Load core/associated repo requirements (unless local-only mode)
118
- if mode in ("combined", "core-only"):
119
- core_path = config.get("core", {}).get("path")
120
- if core_path:
121
- core_reqs = load_requirements_from_repo(Path(core_path), config)
122
- for req_id, req in core_reqs.items():
123
- # Don't overwrite local requirements with same ID
124
- if req_id not in requirements:
125
- requirements[req_id] = RequirementNode.from_core(req)
126
-
127
- if not requirements:
128
- print("Warning: No requirements found", file=sys.stderr)
129
-
130
- return requirements
131
-
132
-
133
- def build_hierarchy(requirements: Dict[str, RequirementNode]) -> Dict[str, RequirementNode]:
134
- """
135
- Compute children for each requirement by inverting implements relationships.
136
-
137
- This modifies the requirements dict in-place, populating each node's
138
- children list.
139
- """
140
- for req_id, node in requirements.items():
141
- for parent_id in node.implements:
142
- # Normalize parent ID format
143
- parent_key = parent_id if parent_id.startswith('REQ-') else f"REQ-{parent_id}"
144
- if parent_key in requirements:
145
- requirements[parent_key].children.append(req_id)
146
-
147
- # Sort children for deterministic traversal
148
- for node in requirements.values():
149
- node.children.sort()
150
-
151
- return requirements
152
-
153
-
154
- def traverse_top_down(
155
- requirements: Dict[str, RequirementNode],
156
- start_req: str,
157
- max_depth: Optional[int] = None,
158
- callback: Optional[Callable[[RequirementNode, int], None]] = None
159
- ) -> List[str]:
160
- """
161
- Traverse hierarchy from start_req downward using BFS.
162
-
163
- Args:
164
- requirements: All requirements with children computed
165
- start_req: Starting REQ ID (e.g., 'REQ-p00044')
166
- max_depth: Maximum depth to traverse (None = unlimited)
167
- callback: Function to call for each REQ visited (node, depth)
168
-
169
- Returns:
170
- List of REQ IDs in traversal order
171
- """
172
- visited = []
173
- queue = [(start_req, 0)] # (req_id, depth)
174
- seen = set()
175
-
176
- while queue:
177
- req_id, depth = queue.pop(0)
178
-
179
- if req_id in seen:
180
- continue
181
-
182
- # Depth limit check (depth 0 is the start node)
183
- if max_depth is not None and depth > max_depth:
184
- continue
185
-
186
- seen.add(req_id)
187
-
188
- if req_id not in requirements:
189
- print(f"Warning: {req_id} not found in requirements", file=sys.stderr)
190
- continue
191
-
192
- visited.append(req_id)
193
- node = requirements[req_id]
194
-
195
- if callback:
196
- callback(node, depth)
197
-
198
- # Add children to queue
199
- for child_id in node.children:
200
- if child_id not in seen:
201
- queue.append((child_id, depth + 1))
202
-
203
- return visited
204
-
205
-
206
- def normalize_req_id(req_id: str, validator: Optional["PatternValidator"] = None) -> str:
207
- """
208
- Normalize requirement ID to canonical format using PatternValidator.
209
-
210
- Args:
211
- req_id: Requirement ID (e.g., "d00027", "REQ-d00027", "REQ-CAL-p00001")
212
- validator: PatternValidator instance (created from config if not provided)
213
-
214
- Returns:
215
- Normalized ID in canonical format from config
216
- """
217
- from elspais.config.loader import load_config, find_config_file
218
- from elspais.core.patterns import PatternValidator, PatternConfig
219
-
220
- # Create validator if not provided
221
- if validator is None:
222
- try:
223
- config_path = find_config_file(Path.cwd())
224
- config = load_config(config_path) if config_path else {}
225
- except Exception:
226
- config = {}
227
- pattern_config = PatternConfig.from_dict(config.get("patterns", {}))
228
- validator = PatternValidator(pattern_config)
229
-
230
- # Try parsing the ID as-is
231
- parsed = validator.parse(req_id)
232
-
233
- # If that fails, try with prefix
234
- if parsed is None and not req_id.upper().startswith(validator.config.prefix):
235
- parsed = validator.parse(f"{validator.config.prefix}-{req_id}")
236
-
237
- if parsed:
238
- # Reconstruct canonical ID from parsed components
239
- parts = [parsed.prefix]
240
- if parsed.associated:
241
- parts.append(parsed.associated)
242
- parts.append(f"{parsed.type_code}{parsed.number}")
243
- return "-".join(parts)
244
-
245
- # Return as-is if unparseable
246
- return req_id