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/trace_view/coverage.py
DELETED
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
elspais.trace_view.coverage - Coverage calculation for trace-view.
|
|
3
|
-
|
|
4
|
-
Provides functions to calculate implementation coverage and status
|
|
5
|
-
for requirements.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from typing import Dict, List
|
|
9
|
-
|
|
10
|
-
from elspais.trace_view.models import TraceViewRequirement
|
|
11
|
-
|
|
12
|
-
# Type alias for requirement dict (supports both ID forms)
|
|
13
|
-
ReqDict = Dict[str, TraceViewRequirement]
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def count_by_level(requirements: ReqDict) -> Dict[str, Dict[str, int]]:
|
|
17
|
-
"""Count requirements by level, both including and excluding Deprecated.
|
|
18
|
-
|
|
19
|
-
Args:
|
|
20
|
-
requirements: Dict mapping requirement ID to TraceViewRequirement
|
|
21
|
-
|
|
22
|
-
Returns:
|
|
23
|
-
Dict with 'active' (excludes Deprecated) and 'all' (includes Deprecated) counts
|
|
24
|
-
Each contains counts for 'PRD', 'OPS', 'DEV'
|
|
25
|
-
"""
|
|
26
|
-
counts = {
|
|
27
|
-
"active": {"PRD": 0, "OPS": 0, "DEV": 0},
|
|
28
|
-
"all": {"PRD": 0, "OPS": 0, "DEV": 0},
|
|
29
|
-
}
|
|
30
|
-
for req in requirements.values():
|
|
31
|
-
level = req.level
|
|
32
|
-
counts["all"][level] = counts["all"].get(level, 0) + 1
|
|
33
|
-
if req.status != "Deprecated":
|
|
34
|
-
counts["active"][level] = counts["active"].get(level, 0) + 1
|
|
35
|
-
return counts
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def find_orphaned_requirements(requirements: ReqDict) -> List[TraceViewRequirement]:
|
|
39
|
-
"""Find requirements not linked from any parent.
|
|
40
|
-
|
|
41
|
-
Args:
|
|
42
|
-
requirements: Dict mapping requirement ID to TraceViewRequirement
|
|
43
|
-
|
|
44
|
-
Returns:
|
|
45
|
-
List of orphaned requirements (non-PRD requirements with no implements)
|
|
46
|
-
"""
|
|
47
|
-
implemented = set()
|
|
48
|
-
for req in requirements.values():
|
|
49
|
-
implemented.update(req.implements)
|
|
50
|
-
|
|
51
|
-
orphaned = []
|
|
52
|
-
for req in requirements.values():
|
|
53
|
-
# Skip PRD requirements (they're top-level)
|
|
54
|
-
if req.level == "PRD":
|
|
55
|
-
continue
|
|
56
|
-
# Skip if this requirement is implemented by someone
|
|
57
|
-
if req.id in implemented:
|
|
58
|
-
continue
|
|
59
|
-
# Skip if it has no parent (should have one)
|
|
60
|
-
if not req.implements:
|
|
61
|
-
orphaned.append(req)
|
|
62
|
-
|
|
63
|
-
return sorted(orphaned, key=lambda r: r.id)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def calculate_coverage(requirements: ReqDict, req_id: str) -> dict:
|
|
67
|
-
"""Calculate coverage for a requirement.
|
|
68
|
-
|
|
69
|
-
Args:
|
|
70
|
-
requirements: Dict mapping requirement ID to TraceViewRequirement
|
|
71
|
-
req_id: ID of requirement to calculate coverage for
|
|
72
|
-
|
|
73
|
-
Returns:
|
|
74
|
-
Dict with 'children' (total child count) and 'traced' (children with implementation)
|
|
75
|
-
"""
|
|
76
|
-
# Find all requirements that implement this requirement (children)
|
|
77
|
-
children = [r for r in requirements.values() if req_id in r.implements]
|
|
78
|
-
|
|
79
|
-
# Count how many children have implementation files or their own children with implementation
|
|
80
|
-
traced = 0
|
|
81
|
-
for child in children:
|
|
82
|
-
child_status = get_implementation_status(requirements, child.id)
|
|
83
|
-
if child_status in ["Full", "Partial"]:
|
|
84
|
-
traced += 1
|
|
85
|
-
|
|
86
|
-
return {"children": len(children), "traced": traced}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def get_implementation_status(requirements: ReqDict, req_id: str) -> str:
|
|
90
|
-
"""Get implementation status for a requirement.
|
|
91
|
-
|
|
92
|
-
Args:
|
|
93
|
-
requirements: Dict mapping requirement ID to TraceViewRequirement
|
|
94
|
-
req_id: ID of requirement to check
|
|
95
|
-
|
|
96
|
-
Returns:
|
|
97
|
-
'Unimplemented': No children AND no implementation_files
|
|
98
|
-
'Partial': Some but not all children traced
|
|
99
|
-
'Full': Has implementation_files OR all children traced
|
|
100
|
-
"""
|
|
101
|
-
req = requirements.get(req_id)
|
|
102
|
-
if not req:
|
|
103
|
-
return "Unimplemented"
|
|
104
|
-
|
|
105
|
-
# If requirement has implementation files, it's fully implemented
|
|
106
|
-
if req.implementation_files:
|
|
107
|
-
return "Full"
|
|
108
|
-
|
|
109
|
-
# Find children
|
|
110
|
-
children = [r for r in requirements.values() if req_id in r.implements]
|
|
111
|
-
|
|
112
|
-
# No children and no implementation files = Unimplemented
|
|
113
|
-
if not children:
|
|
114
|
-
return "Unimplemented"
|
|
115
|
-
|
|
116
|
-
# Check how many children are traced
|
|
117
|
-
coverage = calculate_coverage(requirements, req_id)
|
|
118
|
-
|
|
119
|
-
if coverage["traced"] == 0:
|
|
120
|
-
return "Unimplemented"
|
|
121
|
-
elif coverage["traced"] == coverage["children"]:
|
|
122
|
-
return "Full"
|
|
123
|
-
else:
|
|
124
|
-
return "Partial"
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def generate_coverage_report(requirements: ReqDict, get_status_fn=None) -> str:
|
|
128
|
-
"""Generate text-based coverage report with summary statistics.
|
|
129
|
-
|
|
130
|
-
Args:
|
|
131
|
-
requirements: Dict mapping requirement ID to TraceViewRequirement
|
|
132
|
-
get_status_fn: Optional function to get implementation status.
|
|
133
|
-
If None, uses get_implementation_status.
|
|
134
|
-
|
|
135
|
-
Returns:
|
|
136
|
-
Formatted text report showing:
|
|
137
|
-
- Total requirements count
|
|
138
|
-
- Breakdown by level (PRD, OPS, DEV) with percentages
|
|
139
|
-
- Breakdown by implementation status (Full/Partial/Unimplemented)
|
|
140
|
-
"""
|
|
141
|
-
if get_status_fn is None:
|
|
142
|
-
|
|
143
|
-
def get_status_fn(req_id):
|
|
144
|
-
return get_implementation_status(requirements, req_id)
|
|
145
|
-
|
|
146
|
-
lines = []
|
|
147
|
-
lines.append("=== Coverage Report ===")
|
|
148
|
-
lines.append(f"Total Requirements: {len(requirements)}")
|
|
149
|
-
lines.append("")
|
|
150
|
-
|
|
151
|
-
# Count by level
|
|
152
|
-
by_level = {"PRD": 0, "OPS": 0, "DEV": 0}
|
|
153
|
-
implemented_by_level = {"PRD": 0, "OPS": 0, "DEV": 0}
|
|
154
|
-
|
|
155
|
-
for req in requirements.values():
|
|
156
|
-
level = req.level
|
|
157
|
-
by_level[level] = by_level.get(level, 0) + 1
|
|
158
|
-
|
|
159
|
-
impl_status = get_status_fn(req.id)
|
|
160
|
-
if impl_status in ["Full", "Partial"]:
|
|
161
|
-
implemented_by_level[level] = implemented_by_level.get(level, 0) + 1
|
|
162
|
-
|
|
163
|
-
lines.append("By Level:")
|
|
164
|
-
for level in ["PRD", "OPS", "DEV"]:
|
|
165
|
-
total = by_level[level]
|
|
166
|
-
implemented = implemented_by_level[level]
|
|
167
|
-
percentage = (implemented / total * 100) if total > 0 else 0
|
|
168
|
-
lines.append(f" {level}: {total} ({percentage:.0f}% implemented)")
|
|
169
|
-
|
|
170
|
-
lines.append("")
|
|
171
|
-
|
|
172
|
-
# Count by implementation status
|
|
173
|
-
status_counts = {"Full": 0, "Partial": 0, "Unimplemented": 0}
|
|
174
|
-
for req in requirements.values():
|
|
175
|
-
impl_status = get_status_fn(req.id)
|
|
176
|
-
status_counts[impl_status] = status_counts.get(impl_status, 0) + 1
|
|
177
|
-
|
|
178
|
-
lines.append("By Status:")
|
|
179
|
-
lines.append(f" Full: {status_counts['Full']}")
|
|
180
|
-
lines.append(f" Partial: {status_counts['Partial']}")
|
|
181
|
-
lines.append(f" Unimplemented: {status_counts['Unimplemented']}")
|
|
182
|
-
|
|
183
|
-
return "\n".join(lines)
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# Implements: REQ-int-d00001-A (trace_view package structure)
|
|
2
|
-
"""
|
|
3
|
-
elspais.trace_view.generators - Output format generators.
|
|
4
|
-
|
|
5
|
-
Provides Markdown and CSV generators (no dependencies).
|
|
6
|
-
HTML generator is in the html/ subpackage (requires jinja2).
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from elspais.trace_view.generators.csv import generate_csv
|
|
10
|
-
from elspais.trace_view.generators.markdown import generate_markdown
|
|
11
|
-
|
|
12
|
-
__all__ = ["generate_markdown", "generate_csv"]
|
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
# Implements: REQ-tv-p00001 (TraceViewGenerator)
|
|
2
|
-
"""
|
|
3
|
-
elspais.trace_view.generators.base - Base generator for trace-view.
|
|
4
|
-
|
|
5
|
-
Provides the main TraceViewGenerator class that orchestrates
|
|
6
|
-
requirement parsing, implementation scanning, and output generation.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from typing import Dict, List, Optional
|
|
11
|
-
|
|
12
|
-
from elspais.config.defaults import DEFAULT_CONFIG
|
|
13
|
-
from elspais.config.loader import find_config_file, get_spec_directories, load_config
|
|
14
|
-
from elspais.core.git import get_git_changes
|
|
15
|
-
from elspais.core.parser import RequirementParser
|
|
16
|
-
from elspais.core.patterns import PatternConfig
|
|
17
|
-
from elspais.trace_view.coverage import (
|
|
18
|
-
calculate_coverage,
|
|
19
|
-
generate_coverage_report,
|
|
20
|
-
get_implementation_status,
|
|
21
|
-
)
|
|
22
|
-
from elspais.trace_view.generators.csv import generate_csv, generate_planning_csv
|
|
23
|
-
from elspais.trace_view.generators.markdown import generate_markdown
|
|
24
|
-
from elspais.trace_view.models import GitChangeInfo as TVGitChangeInfo
|
|
25
|
-
from elspais.trace_view.models import TraceViewRequirement
|
|
26
|
-
from elspais.trace_view.scanning import scan_implementation_files
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class TraceViewGenerator:
|
|
30
|
-
"""Generates traceability matrices.
|
|
31
|
-
|
|
32
|
-
This is the main entry point for generating traceability reports.
|
|
33
|
-
Supports multiple output formats: markdown, html, csv.
|
|
34
|
-
|
|
35
|
-
Args:
|
|
36
|
-
spec_dir: Path to the spec directory containing requirement files
|
|
37
|
-
impl_dirs: List of directories to scan for implementation references
|
|
38
|
-
sponsor: Sponsor name for sponsor-specific reports
|
|
39
|
-
mode: Report mode ('core', 'sponsor', 'combined')
|
|
40
|
-
repo_root: Repository root path for relative path calculation
|
|
41
|
-
associated_repos: List of associated repo dicts for multi-repo scanning
|
|
42
|
-
config: Optional pre-loaded configuration dict
|
|
43
|
-
"""
|
|
44
|
-
|
|
45
|
-
# Version number - increment with each change
|
|
46
|
-
VERSION = 17
|
|
47
|
-
|
|
48
|
-
def __init__(
|
|
49
|
-
self,
|
|
50
|
-
spec_dir: Optional[Path] = None,
|
|
51
|
-
impl_dirs: Optional[List[Path]] = None,
|
|
52
|
-
sponsor: Optional[str] = None,
|
|
53
|
-
mode: str = "core",
|
|
54
|
-
repo_root: Optional[Path] = None,
|
|
55
|
-
associated_repos: Optional[list] = None,
|
|
56
|
-
config: Optional[dict] = None,
|
|
57
|
-
):
|
|
58
|
-
self.spec_dir = spec_dir
|
|
59
|
-
self.requirements: Dict[str, TraceViewRequirement] = {}
|
|
60
|
-
self.impl_dirs = impl_dirs or []
|
|
61
|
-
self.sponsor = sponsor
|
|
62
|
-
self.mode = mode
|
|
63
|
-
self.repo_root = repo_root or (spec_dir.parent if spec_dir else Path.cwd())
|
|
64
|
-
self.associated_repos = associated_repos or []
|
|
65
|
-
self._base_path = ""
|
|
66
|
-
self._config = config
|
|
67
|
-
self._git_info: Optional[TVGitChangeInfo] = None
|
|
68
|
-
|
|
69
|
-
def generate(
|
|
70
|
-
self,
|
|
71
|
-
format: str = "markdown",
|
|
72
|
-
output_file: Optional[Path] = None,
|
|
73
|
-
embed_content: bool = False,
|
|
74
|
-
edit_mode: bool = False,
|
|
75
|
-
review_mode: bool = False,
|
|
76
|
-
quiet: bool = False,
|
|
77
|
-
) -> str:
|
|
78
|
-
"""Generate traceability matrix in specified format.
|
|
79
|
-
|
|
80
|
-
Args:
|
|
81
|
-
format: Output format ('markdown', 'html', 'csv')
|
|
82
|
-
output_file: Path to write output (default: traceability_matrix.{ext})
|
|
83
|
-
embed_content: If True, embed full requirement content in HTML
|
|
84
|
-
edit_mode: If True, include edit mode UI in HTML output
|
|
85
|
-
review_mode: If True, include review mode UI in HTML output
|
|
86
|
-
quiet: If True, suppress progress messages
|
|
87
|
-
|
|
88
|
-
Returns:
|
|
89
|
-
The generated content as a string
|
|
90
|
-
"""
|
|
91
|
-
# Initialize git state
|
|
92
|
-
self._init_git_state(quiet)
|
|
93
|
-
|
|
94
|
-
# Parse requirements
|
|
95
|
-
if not quiet:
|
|
96
|
-
print("Scanning for requirements...")
|
|
97
|
-
self._parse_requirements(quiet)
|
|
98
|
-
|
|
99
|
-
if not self.requirements:
|
|
100
|
-
if not quiet:
|
|
101
|
-
print("Warning: No requirements found")
|
|
102
|
-
return ""
|
|
103
|
-
|
|
104
|
-
if not quiet:
|
|
105
|
-
print(f"Found {len(self.requirements)} requirements")
|
|
106
|
-
|
|
107
|
-
# Pre-detect cycles and mark affected requirements
|
|
108
|
-
self._detect_and_mark_cycles(quiet)
|
|
109
|
-
|
|
110
|
-
# Scan implementation files
|
|
111
|
-
if self.impl_dirs:
|
|
112
|
-
if not quiet:
|
|
113
|
-
print("Scanning implementation files...")
|
|
114
|
-
scan_implementation_files(
|
|
115
|
-
self.requirements,
|
|
116
|
-
self.impl_dirs,
|
|
117
|
-
self.repo_root,
|
|
118
|
-
self.mode,
|
|
119
|
-
self.sponsor,
|
|
120
|
-
quiet=quiet,
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
if not quiet:
|
|
124
|
-
print(f"Generating {format.upper()} traceability matrix...")
|
|
125
|
-
|
|
126
|
-
# Determine output path and extension
|
|
127
|
-
if format == "html":
|
|
128
|
-
ext = ".html"
|
|
129
|
-
elif format == "csv":
|
|
130
|
-
ext = ".csv"
|
|
131
|
-
else:
|
|
132
|
-
ext = ".md"
|
|
133
|
-
|
|
134
|
-
if output_file is None:
|
|
135
|
-
output_file = Path(f"traceability_matrix{ext}")
|
|
136
|
-
|
|
137
|
-
# Calculate relative path for links
|
|
138
|
-
self._calculate_base_path(output_file)
|
|
139
|
-
|
|
140
|
-
# Generate content
|
|
141
|
-
if format == "html":
|
|
142
|
-
from elspais.trace_view.html import HTMLGenerator
|
|
143
|
-
|
|
144
|
-
html_gen = HTMLGenerator(
|
|
145
|
-
requirements=self.requirements,
|
|
146
|
-
base_path=self._base_path,
|
|
147
|
-
mode=self.mode,
|
|
148
|
-
sponsor=self.sponsor,
|
|
149
|
-
version=self.VERSION,
|
|
150
|
-
repo_root=self.repo_root,
|
|
151
|
-
)
|
|
152
|
-
content = html_gen.generate(
|
|
153
|
-
embed_content=embed_content, edit_mode=edit_mode, review_mode=review_mode
|
|
154
|
-
)
|
|
155
|
-
elif format == "csv":
|
|
156
|
-
content = generate_csv(self.requirements)
|
|
157
|
-
else:
|
|
158
|
-
content = generate_markdown(self.requirements, self._base_path)
|
|
159
|
-
|
|
160
|
-
# Write output file
|
|
161
|
-
output_file.write_text(content)
|
|
162
|
-
if not quiet:
|
|
163
|
-
print(f"Traceability matrix written to: {output_file}")
|
|
164
|
-
|
|
165
|
-
return content
|
|
166
|
-
|
|
167
|
-
def _init_git_state(self, quiet: bool = False):
|
|
168
|
-
"""Initialize git state for requirement status detection."""
|
|
169
|
-
try:
|
|
170
|
-
git_changes = get_git_changes(self.repo_root)
|
|
171
|
-
|
|
172
|
-
# Convert to trace_view GitChangeInfo
|
|
173
|
-
self._git_info = TVGitChangeInfo(
|
|
174
|
-
uncommitted_files=git_changes.uncommitted_files,
|
|
175
|
-
untracked_files=git_changes.untracked_files,
|
|
176
|
-
branch_changed_files=git_changes.branch_changed_files,
|
|
177
|
-
committed_req_locations=git_changes.committed_req_locations,
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
# Report uncommitted changes
|
|
181
|
-
if not quiet and git_changes.uncommitted_files:
|
|
182
|
-
spec_uncommitted = [
|
|
183
|
-
f for f in git_changes.uncommitted_files if f.startswith("spec/")
|
|
184
|
-
]
|
|
185
|
-
if spec_uncommitted:
|
|
186
|
-
print(f"Uncommitted spec files: {len(spec_uncommitted)}")
|
|
187
|
-
|
|
188
|
-
# Report branch changes vs main
|
|
189
|
-
if not quiet and git_changes.branch_changed_files:
|
|
190
|
-
spec_branch = [f for f in git_changes.branch_changed_files if f.startswith("spec/")]
|
|
191
|
-
if spec_branch:
|
|
192
|
-
print(f"Spec files changed vs main: {len(spec_branch)}")
|
|
193
|
-
|
|
194
|
-
except Exception as e:
|
|
195
|
-
# Git state is optional - continue without it
|
|
196
|
-
if not quiet:
|
|
197
|
-
print(f"Warning: Could not get git state: {e}")
|
|
198
|
-
self._git_info = None
|
|
199
|
-
|
|
200
|
-
def _parse_requirements(self, quiet: bool = False):
|
|
201
|
-
"""Parse all requirements using elspais parser directly."""
|
|
202
|
-
# Load config if not provided
|
|
203
|
-
if self._config is None:
|
|
204
|
-
config_path = find_config_file(self.repo_root)
|
|
205
|
-
if config_path and config_path.exists():
|
|
206
|
-
self._config = load_config(config_path)
|
|
207
|
-
else:
|
|
208
|
-
self._config = DEFAULT_CONFIG
|
|
209
|
-
|
|
210
|
-
# Get spec directories
|
|
211
|
-
spec_dirs = get_spec_directories(self.spec_dir, self._config)
|
|
212
|
-
if not spec_dirs:
|
|
213
|
-
return
|
|
214
|
-
|
|
215
|
-
# Parse requirements using elspais parser
|
|
216
|
-
pattern_config = PatternConfig.from_dict(self._config.get("patterns", {}))
|
|
217
|
-
spec_config = self._config.get("spec", {})
|
|
218
|
-
no_reference_values = spec_config.get("no_reference_values")
|
|
219
|
-
skip_files = spec_config.get("skip_files", [])
|
|
220
|
-
|
|
221
|
-
parser = RequirementParser(pattern_config, no_reference_values=no_reference_values)
|
|
222
|
-
parse_result = parser.parse_directories(spec_dirs, skip_files=skip_files)
|
|
223
|
-
|
|
224
|
-
roadmap_count = 0
|
|
225
|
-
conflict_count = 0
|
|
226
|
-
cycle_count = 0
|
|
227
|
-
|
|
228
|
-
for req_id, core_req in parse_result.items():
|
|
229
|
-
# Wrap in TraceViewRequirement
|
|
230
|
-
tv_req = TraceViewRequirement.from_core(core_req, git_info=self._git_info)
|
|
231
|
-
|
|
232
|
-
if tv_req.is_roadmap:
|
|
233
|
-
roadmap_count += 1
|
|
234
|
-
if tv_req.is_conflict:
|
|
235
|
-
conflict_count += 1
|
|
236
|
-
if not quiet:
|
|
237
|
-
print(f" Warning: Conflict: {req_id} conflicts with {tv_req.conflict_with}")
|
|
238
|
-
|
|
239
|
-
# Store by short ID (without REQ- prefix)
|
|
240
|
-
self.requirements[tv_req.id] = tv_req
|
|
241
|
-
|
|
242
|
-
if not quiet:
|
|
243
|
-
if roadmap_count > 0:
|
|
244
|
-
print(f" Found {roadmap_count} roadmap requirements")
|
|
245
|
-
if conflict_count > 0:
|
|
246
|
-
print(f" Found {conflict_count} conflicts")
|
|
247
|
-
if cycle_count > 0:
|
|
248
|
-
print(f" Found {cycle_count} requirements in dependency cycles")
|
|
249
|
-
|
|
250
|
-
def _detect_and_mark_cycles(self, quiet: bool = False):
|
|
251
|
-
"""Detect and mark requirements that are part of dependency cycles."""
|
|
252
|
-
# Simple cycle detection using DFS
|
|
253
|
-
visited = set()
|
|
254
|
-
rec_stack = set()
|
|
255
|
-
cycle_members = set()
|
|
256
|
-
|
|
257
|
-
def dfs(req_id: str, path: List[str]) -> bool:
|
|
258
|
-
if req_id in rec_stack:
|
|
259
|
-
# Found cycle - mark all members in the cycle path
|
|
260
|
-
cycle_start = path.index(req_id)
|
|
261
|
-
for member in path[cycle_start:]:
|
|
262
|
-
cycle_members.add(member)
|
|
263
|
-
return True
|
|
264
|
-
|
|
265
|
-
if req_id in visited:
|
|
266
|
-
return False
|
|
267
|
-
|
|
268
|
-
visited.add(req_id)
|
|
269
|
-
rec_stack.add(req_id)
|
|
270
|
-
|
|
271
|
-
req = self.requirements.get(req_id)
|
|
272
|
-
if req:
|
|
273
|
-
for parent_id in req.implements:
|
|
274
|
-
# Normalize parent_id
|
|
275
|
-
parent_id = parent_id.replace("REQ-", "")
|
|
276
|
-
if parent_id in self.requirements:
|
|
277
|
-
if dfs(parent_id, path + [req_id]):
|
|
278
|
-
cycle_members.add(req_id)
|
|
279
|
-
|
|
280
|
-
rec_stack.remove(req_id)
|
|
281
|
-
return False
|
|
282
|
-
|
|
283
|
-
# Run DFS from each requirement
|
|
284
|
-
for req_id in self.requirements:
|
|
285
|
-
if req_id not in visited:
|
|
286
|
-
dfs(req_id, [])
|
|
287
|
-
|
|
288
|
-
# Clear implements for cycle members so they appear as orphaned
|
|
289
|
-
cycle_count = 0
|
|
290
|
-
for req_id in cycle_members:
|
|
291
|
-
if req_id in self.requirements:
|
|
292
|
-
req = self.requirements[req_id]
|
|
293
|
-
if req.implements:
|
|
294
|
-
# Modify the underlying core requirement
|
|
295
|
-
req.core.implements = []
|
|
296
|
-
cycle_count += 1
|
|
297
|
-
|
|
298
|
-
if not quiet and cycle_count > 0:
|
|
299
|
-
print(
|
|
300
|
-
f" Warning: {cycle_count} requirements marked as cyclic (shown as orphaned items)"
|
|
301
|
-
)
|
|
302
|
-
|
|
303
|
-
def _calculate_base_path(self, output_file: Path):
|
|
304
|
-
"""Calculate relative path from output file location to repo root."""
|
|
305
|
-
try:
|
|
306
|
-
output_dir = output_file.resolve().parent
|
|
307
|
-
repo_root = self.repo_root.resolve()
|
|
308
|
-
|
|
309
|
-
try:
|
|
310
|
-
rel_path = output_dir.relative_to(repo_root)
|
|
311
|
-
depth = len(rel_path.parts)
|
|
312
|
-
if depth == 0:
|
|
313
|
-
self._base_path = ""
|
|
314
|
-
else:
|
|
315
|
-
self._base_path = "../" * depth
|
|
316
|
-
except ValueError:
|
|
317
|
-
self._base_path = f"file://{repo_root}/"
|
|
318
|
-
except Exception:
|
|
319
|
-
self._base_path = "../"
|
|
320
|
-
|
|
321
|
-
def generate_planning_csv(self) -> str:
|
|
322
|
-
"""Generate planning CSV with actionable requirements."""
|
|
323
|
-
|
|
324
|
-
def get_status(req_id):
|
|
325
|
-
return get_implementation_status(self.requirements, req_id)
|
|
326
|
-
|
|
327
|
-
def calc_coverage(req_id):
|
|
328
|
-
return calculate_coverage(self.requirements, req_id)
|
|
329
|
-
|
|
330
|
-
return generate_planning_csv(self.requirements, get_status, calc_coverage)
|
|
331
|
-
|
|
332
|
-
def generate_coverage_report(self) -> str:
|
|
333
|
-
"""Generate coverage report showing implementation status."""
|
|
334
|
-
return generate_coverage_report(self.requirements)
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
elspais.trace_view.generators.csv - CSV generation.
|
|
3
|
-
|
|
4
|
-
Provides functions to generate CSV traceability matrices and planning exports.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import csv
|
|
8
|
-
from io import StringIO
|
|
9
|
-
from typing import Callable, Dict
|
|
10
|
-
|
|
11
|
-
from elspais.trace_view.models import TraceViewRequirement
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def generate_csv(requirements: Dict[str, TraceViewRequirement]) -> str:
|
|
15
|
-
"""Generate CSV traceability matrix.
|
|
16
|
-
|
|
17
|
-
Args:
|
|
18
|
-
requirements: Dict mapping requirement ID to TraceViewRequirement
|
|
19
|
-
|
|
20
|
-
Returns:
|
|
21
|
-
CSV string with columns: Requirement ID, Title, Level, Status,
|
|
22
|
-
Implements, Traced By, File, Line, Implementation Files
|
|
23
|
-
"""
|
|
24
|
-
output = StringIO()
|
|
25
|
-
writer = csv.writer(output)
|
|
26
|
-
|
|
27
|
-
# Header
|
|
28
|
-
writer.writerow(
|
|
29
|
-
[
|
|
30
|
-
"Requirement ID",
|
|
31
|
-
"Title",
|
|
32
|
-
"Level",
|
|
33
|
-
"Status",
|
|
34
|
-
"Implements",
|
|
35
|
-
"Traced By",
|
|
36
|
-
"File",
|
|
37
|
-
"Line",
|
|
38
|
-
"Implementation Files",
|
|
39
|
-
]
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
# Sort requirements by ID
|
|
43
|
-
sorted_reqs = sorted(requirements.values(), key=lambda r: r.id)
|
|
44
|
-
|
|
45
|
-
for req in sorted_reqs:
|
|
46
|
-
# Compute children (traced by) dynamically
|
|
47
|
-
children = [r.id for r in requirements.values() if req.id in r.implements]
|
|
48
|
-
|
|
49
|
-
# Format implementation files as "file:line" strings
|
|
50
|
-
impl_files_str = (
|
|
51
|
-
", ".join([f"{path}:{line}" for path, line in req.implementation_files])
|
|
52
|
-
if req.implementation_files
|
|
53
|
-
else "-"
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
writer.writerow(
|
|
57
|
-
[
|
|
58
|
-
req.id,
|
|
59
|
-
req.title,
|
|
60
|
-
req.level,
|
|
61
|
-
req.status,
|
|
62
|
-
", ".join(req.implements) if req.implements else "-",
|
|
63
|
-
", ".join(sorted(children)) if children else "-",
|
|
64
|
-
req.display_filename,
|
|
65
|
-
req.line_number,
|
|
66
|
-
impl_files_str,
|
|
67
|
-
]
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
return output.getvalue()
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def generate_planning_csv(
|
|
74
|
-
requirements: Dict[str, TraceViewRequirement],
|
|
75
|
-
get_implementation_status: Callable[[str], str],
|
|
76
|
-
calculate_coverage: Callable[[str], dict],
|
|
77
|
-
) -> str:
|
|
78
|
-
"""Generate CSV for sprint planning (actionable items only).
|
|
79
|
-
|
|
80
|
-
Args:
|
|
81
|
-
requirements: Dict mapping requirement ID to TraceViewRequirement
|
|
82
|
-
get_implementation_status: Function that takes req_id and returns status string
|
|
83
|
-
calculate_coverage: Function that takes req_id and returns coverage dict
|
|
84
|
-
|
|
85
|
-
Returns:
|
|
86
|
-
CSV with columns: REQ ID, Title, Level, Status, Impl Status, Coverage, Code Refs
|
|
87
|
-
Includes only actionable items (Active or Draft status, not deprecated)
|
|
88
|
-
"""
|
|
89
|
-
output = StringIO()
|
|
90
|
-
writer = csv.writer(output)
|
|
91
|
-
|
|
92
|
-
# Header
|
|
93
|
-
writer.writerow(["REQ ID", "Title", "Level", "Status", "Impl Status", "Coverage", "Code Refs"])
|
|
94
|
-
|
|
95
|
-
# Filter to actionable requirements (Active or Draft status)
|
|
96
|
-
actionable_reqs = [req for req in requirements.values() if req.status in ["Active", "Draft"]]
|
|
97
|
-
|
|
98
|
-
# Sort by ID
|
|
99
|
-
actionable_reqs.sort(key=lambda r: r.id)
|
|
100
|
-
|
|
101
|
-
for req in actionable_reqs:
|
|
102
|
-
impl_status = get_implementation_status(req.id)
|
|
103
|
-
coverage = calculate_coverage(req.id)
|
|
104
|
-
code_refs = len(req.implementation_files)
|
|
105
|
-
|
|
106
|
-
writer.writerow(
|
|
107
|
-
[
|
|
108
|
-
req.id,
|
|
109
|
-
req.title,
|
|
110
|
-
req.level,
|
|
111
|
-
req.status,
|
|
112
|
-
impl_status,
|
|
113
|
-
f"{coverage['traced']}/{coverage['children']}",
|
|
114
|
-
code_refs,
|
|
115
|
-
]
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
return output.getvalue()
|