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.
Files changed (147) hide show
  1. elspais/__init__.py +1 -10
  2. elspais/{sponsors/__init__.py → associates.py} +102 -56
  3. elspais/cli.py +366 -69
  4. elspais/commands/__init__.py +9 -3
  5. elspais/commands/analyze.py +118 -169
  6. elspais/commands/changed.py +12 -23
  7. elspais/commands/config_cmd.py +10 -13
  8. elspais/commands/edit.py +33 -13
  9. elspais/commands/example_cmd.py +319 -0
  10. elspais/commands/hash_cmd.py +161 -183
  11. elspais/commands/health.py +1177 -0
  12. elspais/commands/index.py +98 -115
  13. elspais/commands/init.py +99 -22
  14. elspais/commands/reformat_cmd.py +41 -433
  15. elspais/commands/rules_cmd.py +2 -2
  16. elspais/commands/trace.py +443 -324
  17. elspais/commands/validate.py +193 -411
  18. elspais/config/__init__.py +799 -5
  19. elspais/{core/content_rules.py → content_rules.py} +20 -2
  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 +45 -29
  58. elspais/mcp/__main__.py +5 -1
  59. elspais/mcp/file_mutations.py +138 -0
  60. elspais/mcp/server.py +1998 -244
  61. elspais/testing/__init__.py +3 -3
  62. elspais/testing/config.py +3 -0
  63. elspais/testing/mapper.py +1 -1
  64. elspais/testing/scanner.py +301 -12
  65. elspais/utilities/__init__.py +1 -0
  66. elspais/utilities/docs_loader.py +115 -0
  67. elspais/utilities/git.py +607 -0
  68. elspais/{core → utilities}/hasher.py +8 -22
  69. elspais/utilities/md_renderer.py +189 -0
  70. elspais/{core → utilities}/patterns.py +56 -51
  71. elspais/utilities/reference_config.py +626 -0
  72. elspais/validation/__init__.py +19 -0
  73. elspais/validation/format.py +264 -0
  74. {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/METADATA +7 -4
  75. elspais-0.43.5.dist-info/RECORD +80 -0
  76. elspais/config/defaults.py +0 -179
  77. elspais/config/loader.py +0 -494
  78. elspais/core/__init__.py +0 -21
  79. elspais/core/git.py +0 -346
  80. elspais/core/models.py +0 -320
  81. elspais/core/parser.py +0 -639
  82. elspais/core/rules.py +0 -509
  83. elspais/mcp/context.py +0 -172
  84. elspais/mcp/serializers.py +0 -112
  85. elspais/reformat/__init__.py +0 -50
  86. elspais/reformat/detector.py +0 -112
  87. elspais/reformat/hierarchy.py +0 -247
  88. elspais/reformat/line_breaks.py +0 -218
  89. elspais/reformat/prompts.py +0 -133
  90. elspais/reformat/transformer.py +0 -266
  91. elspais/trace_view/__init__.py +0 -55
  92. elspais/trace_view/coverage.py +0 -183
  93. elspais/trace_view/generators/__init__.py +0 -12
  94. elspais/trace_view/generators/base.py +0 -334
  95. elspais/trace_view/generators/csv.py +0 -118
  96. elspais/trace_view/generators/markdown.py +0 -170
  97. elspais/trace_view/html/__init__.py +0 -33
  98. elspais/trace_view/html/generator.py +0 -1140
  99. elspais/trace_view/html/templates/base.html +0 -283
  100. elspais/trace_view/html/templates/components/code_viewer_modal.html +0 -14
  101. elspais/trace_view/html/templates/components/file_picker_modal.html +0 -20
  102. elspais/trace_view/html/templates/components/legend_modal.html +0 -69
  103. elspais/trace_view/html/templates/components/review_panel.html +0 -118
  104. elspais/trace_view/html/templates/partials/review/help/help-panel.json +0 -244
  105. elspais/trace_view/html/templates/partials/review/help/onboarding.json +0 -77
  106. elspais/trace_view/html/templates/partials/review/help/tooltips.json +0 -237
  107. elspais/trace_view/html/templates/partials/review/review-comments.js +0 -928
  108. elspais/trace_view/html/templates/partials/review/review-data.js +0 -961
  109. elspais/trace_view/html/templates/partials/review/review-help.js +0 -679
  110. elspais/trace_view/html/templates/partials/review/review-init.js +0 -177
  111. elspais/trace_view/html/templates/partials/review/review-line-numbers.js +0 -429
  112. elspais/trace_view/html/templates/partials/review/review-packages.js +0 -1029
  113. elspais/trace_view/html/templates/partials/review/review-position.js +0 -540
  114. elspais/trace_view/html/templates/partials/review/review-resize.js +0 -115
  115. elspais/trace_view/html/templates/partials/review/review-status.js +0 -659
  116. elspais/trace_view/html/templates/partials/review/review-sync.js +0 -992
  117. elspais/trace_view/html/templates/partials/review-styles.css +0 -2238
  118. elspais/trace_view/html/templates/partials/scripts.js +0 -1741
  119. elspais/trace_view/html/templates/partials/styles.css +0 -1756
  120. elspais/trace_view/models.py +0 -378
  121. elspais/trace_view/review/__init__.py +0 -63
  122. elspais/trace_view/review/branches.py +0 -1142
  123. elspais/trace_view/review/models.py +0 -1200
  124. elspais/trace_view/review/position.py +0 -591
  125. elspais/trace_view/review/server.py +0 -1032
  126. elspais/trace_view/review/status.py +0 -455
  127. elspais/trace_view/review/storage.py +0 -1343
  128. elspais/trace_view/scanning.py +0 -213
  129. elspais/trace_view/specs/README.md +0 -84
  130. elspais/trace_view/specs/tv-d00001-template-architecture.md +0 -36
  131. elspais/trace_view/specs/tv-d00002-css-extraction.md +0 -37
  132. elspais/trace_view/specs/tv-d00003-js-extraction.md +0 -43
  133. elspais/trace_view/specs/tv-d00004-build-embedding.md +0 -40
  134. elspais/trace_view/specs/tv-d00005-test-format.md +0 -78
  135. elspais/trace_view/specs/tv-d00010-review-data-models.md +0 -33
  136. elspais/trace_view/specs/tv-d00011-review-storage.md +0 -33
  137. elspais/trace_view/specs/tv-d00012-position-resolution.md +0 -33
  138. elspais/trace_view/specs/tv-d00013-git-branches.md +0 -31
  139. elspais/trace_view/specs/tv-d00014-review-api-server.md +0 -31
  140. elspais/trace_view/specs/tv-d00015-status-modifier.md +0 -27
  141. elspais/trace_view/specs/tv-d00016-js-integration.md +0 -33
  142. elspais/trace_view/specs/tv-p00001-html-generator.md +0 -33
  143. elspais/trace_view/specs/tv-p00002-review-system.md +0 -29
  144. elspais-0.11.2.dist-info/RECORD +0 -101
  145. {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/WHEEL +0 -0
  146. {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/entry_points.txt +0 -0
  147. {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/licenses/LICENSE +0 -0
@@ -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()