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