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,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, Union
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(
128
- requirements: ReqDict, get_status_fn=None
129
- ) -> str:
130
- """Generate text-based coverage report with summary statistics.
131
-
132
- Args:
133
- requirements: Dict mapping requirement ID to TraceViewRequirement
134
- get_status_fn: Optional function to get implementation status.
135
- If None, uses get_implementation_status.
136
-
137
- Returns:
138
- Formatted text report showing:
139
- - Total requirements count
140
- - Breakdown by level (PRD, OPS, DEV) with percentages
141
- - Breakdown by implementation status (Full/Partial/Unimplemented)
142
- """
143
- if get_status_fn is None:
144
- get_status_fn = lambda req_id: 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.markdown import generate_markdown
10
- from elspais.trace_view.generators.csv import generate_csv
11
-
12
- __all__ = ["generate_markdown", "generate_csv"]
@@ -1,329 +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.loader import find_config_file, load_config, get_spec_directories
13
- from elspais.config.defaults import DEFAULT_CONFIG
14
- from elspais.core.parser import RequirementParser
15
- from elspais.core.patterns import PatternConfig
16
- from elspais.core.git import get_git_changes, GitChangeInfo
17
-
18
- from elspais.trace_view.models import TraceViewRequirement, GitChangeInfo as TVGitChangeInfo
19
- from elspais.trace_view.scanning import scan_implementation_files
20
- from elspais.trace_view.coverage import (
21
- calculate_coverage,
22
- generate_coverage_report,
23
- get_implementation_status,
24
- )
25
- from elspais.trace_view.generators.csv import generate_csv, generate_planning_csv
26
- from elspais.trace_view.generators.markdown import generate_markdown
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(f"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 = [
191
- f for f in git_changes.branch_changed_files if f.startswith("spec/")
192
- ]
193
- if spec_branch:
194
- print(f"Spec files changed vs main: {len(spec_branch)}")
195
-
196
- except Exception as e:
197
- # Git state is optional - continue without it
198
- if not quiet:
199
- print(f"Warning: Could not get git state: {e}")
200
- self._git_info = None
201
-
202
- def _parse_requirements(self, quiet: bool = False):
203
- """Parse all requirements using elspais parser directly."""
204
- # Load config if not provided
205
- if self._config is None:
206
- config_path = find_config_file(self.repo_root)
207
- if config_path and config_path.exists():
208
- self._config = load_config(config_path)
209
- else:
210
- self._config = DEFAULT_CONFIG
211
-
212
- # Get spec directories
213
- spec_dirs = get_spec_directories(self.spec_dir, self._config)
214
- if not spec_dirs:
215
- return
216
-
217
- # Parse requirements using elspais parser
218
- pattern_config = PatternConfig.from_dict(self._config.get("patterns", {}))
219
- spec_config = self._config.get("spec", {})
220
- no_reference_values = spec_config.get("no_reference_values")
221
- skip_files = spec_config.get("skip_files", [])
222
-
223
- parser = RequirementParser(pattern_config, no_reference_values=no_reference_values)
224
- parse_result = parser.parse_directories(spec_dirs, skip_files=skip_files)
225
-
226
- roadmap_count = 0
227
- conflict_count = 0
228
- cycle_count = 0
229
-
230
- for req_id, core_req in parse_result.items():
231
- # Wrap in TraceViewRequirement
232
- tv_req = TraceViewRequirement.from_core(core_req, git_info=self._git_info)
233
-
234
- if tv_req.is_roadmap:
235
- roadmap_count += 1
236
- if tv_req.is_conflict:
237
- conflict_count += 1
238
- if not quiet:
239
- print(f" Warning: Conflict: {req_id} conflicts with {tv_req.conflict_with}")
240
-
241
- # Store by short ID (without REQ- prefix)
242
- self.requirements[tv_req.id] = tv_req
243
-
244
- if not quiet:
245
- if roadmap_count > 0:
246
- print(f" Found {roadmap_count} roadmap requirements")
247
- if conflict_count > 0:
248
- print(f" Found {conflict_count} conflicts")
249
- if cycle_count > 0:
250
- print(f" Found {cycle_count} requirements in dependency cycles")
251
-
252
- def _detect_and_mark_cycles(self, quiet: bool = False):
253
- """Detect and mark requirements that are part of dependency cycles."""
254
- # Simple cycle detection using DFS
255
- visited = set()
256
- rec_stack = set()
257
- cycle_members = set()
258
-
259
- def dfs(req_id: str, path: List[str]) -> bool:
260
- if req_id in rec_stack:
261
- # Found cycle - mark all members in the cycle path
262
- cycle_start = path.index(req_id)
263
- for member in path[cycle_start:]:
264
- cycle_members.add(member)
265
- return True
266
-
267
- if req_id in visited:
268
- return False
269
-
270
- visited.add(req_id)
271
- rec_stack.add(req_id)
272
-
273
- req = self.requirements.get(req_id)
274
- if req:
275
- for parent_id in req.implements:
276
- # Normalize parent_id
277
- parent_id = parent_id.replace("REQ-", "")
278
- if parent_id in self.requirements:
279
- if dfs(parent_id, path + [req_id]):
280
- cycle_members.add(req_id)
281
-
282
- rec_stack.remove(req_id)
283
- return False
284
-
285
- # Run DFS from each requirement
286
- for req_id in self.requirements:
287
- if req_id not in visited:
288
- dfs(req_id, [])
289
-
290
- # Clear implements for cycle members so they appear as orphaned
291
- cycle_count = 0
292
- for req_id in cycle_members:
293
- if req_id in self.requirements:
294
- req = self.requirements[req_id]
295
- if req.implements:
296
- # Modify the underlying core requirement
297
- req.core.implements = []
298
- cycle_count += 1
299
-
300
- if not quiet and cycle_count > 0:
301
- print(f" Warning: {cycle_count} requirements marked as cyclic (shown as orphaned items)")
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
- get_status = lambda req_id: get_implementation_status(self.requirements, req_id)
324
- calc_coverage = lambda req_id: calculate_coverage(self.requirements, req_id)
325
- return generate_planning_csv(self.requirements, get_status, calc_coverage)
326
-
327
- def generate_coverage_report(self) -> str:
328
- """Generate coverage report showing implementation status."""
329
- return generate_coverage_report(self.requirements)
@@ -1,122 +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(
94
- ["REQ ID", "Title", "Level", "Status", "Impl Status", "Coverage", "Code Refs"]
95
- )
96
-
97
- # Filter to actionable requirements (Active or Draft status)
98
- actionable_reqs = [
99
- req for req in requirements.values() if req.status in ["Active", "Draft"]
100
- ]
101
-
102
- # Sort by ID
103
- actionable_reqs.sort(key=lambda r: r.id)
104
-
105
- for req in actionable_reqs:
106
- impl_status = get_implementation_status(req.id)
107
- coverage = calculate_coverage(req.id)
108
- code_refs = len(req.implementation_files)
109
-
110
- writer.writerow(
111
- [
112
- req.id,
113
- req.title,
114
- req.level,
115
- req.status,
116
- impl_status,
117
- f"{coverage['traced']}/{coverage['children']}",
118
- code_refs,
119
- ]
120
- )
121
-
122
- return output.getvalue()