switchforge 1.0.0__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.
@@ -0,0 +1,72 @@
1
+ """Pydantic models for project structure — 4-Level DAG."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class Component(BaseModel):
9
+ """A single class/file within a layer."""
10
+
11
+ name: str
12
+ file_path: str
13
+ layer: str = ""
14
+ dependencies: list[str] = Field(default_factory=list)
15
+ is_tested: bool = False
16
+ existing_test_file: str = ""
17
+ coverage_pct: float = 0.0
18
+
19
+
20
+ class Journey(BaseModel):
21
+ """A traced user journey across layers."""
22
+
23
+ name: str
24
+ entry_point: str
25
+ entry_type: str = "" # route, consumer, job, grpc, cli
26
+ components: list[str] = Field(default_factory=list) # ordered list of component names
27
+ priority: int = 1 # 1 = critical, 5 = low
28
+ description: str = ""
29
+
30
+
31
+ class Layer(BaseModel):
32
+ """A functional layer within a module (e.g., controllers, services, adapters)."""
33
+
34
+ name: str
35
+ components: list[Component] = Field(default_factory=list)
36
+
37
+
38
+ class Module(BaseModel):
39
+ """A module/package within the project."""
40
+
41
+ name: str
42
+ path: str
43
+ layers: list[Layer] = Field(default_factory=list)
44
+ journeys: list[Journey] = Field(default_factory=list)
45
+
46
+
47
+ class TechStack(BaseModel):
48
+ """Detected technology stack."""
49
+
50
+ language: str = ""
51
+ framework: str = ""
52
+ build_tool: str = ""
53
+ test_framework: str = ""
54
+ mock_library: str = ""
55
+ coverage_tool: str = ""
56
+ source_root: str = ""
57
+ test_root: str = ""
58
+ test_command: str = ""
59
+ coverage_command: str = ""
60
+ is_monorepo: bool = False
61
+ modules: list[str] = Field(default_factory=list)
62
+
63
+
64
+ class ProjectGraph(BaseModel):
65
+ """4-Level DAG: Project → Modules → Layers → Components."""
66
+
67
+ name: str = ""
68
+ root_path: str = ""
69
+ tech_stack: TechStack = Field(default_factory=TechStack)
70
+ modules: list[Module] = Field(default_factory=list)
71
+ total_source_files: int = 0
72
+ total_test_files: int = 0
@@ -0,0 +1,93 @@
1
+ """Pydantic models for test results and coverage reports."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from typing import Optional
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ class TestFileResult(BaseModel):
12
+ """Result of a single generated test file."""
13
+
14
+ file_path: str
15
+ test_count: int = 0
16
+ passed: int = 0
17
+ failed: int = 0
18
+ errors: list[str] = Field(default_factory=list)
19
+ compile_errors: list[str] = Field(default_factory=list)
20
+
21
+
22
+ class CoverageEntry(BaseModel):
23
+ """Coverage data for a single source file."""
24
+
25
+ file_path: str
26
+ line_coverage: float = 0.0
27
+ branch_coverage: float = 0.0
28
+ lines_covered: int = 0
29
+ lines_total: int = 0
30
+
31
+
32
+ class CoverageReport(BaseModel):
33
+ """Aggregated coverage report."""
34
+
35
+ line_coverage: float = 0.0
36
+ branch_coverage: float = 0.0
37
+ files: list[CoverageEntry] = Field(default_factory=list)
38
+ total_lines_covered: int = 0
39
+ total_lines: int = 0
40
+ total_tests: int = 0
41
+ tests_passed: int = 0
42
+ tests_failed: int = 0
43
+
44
+
45
+ class IterationResult(BaseModel):
46
+ """Result of a single generation iteration."""
47
+
48
+ iteration: int
49
+ tests_generated: list[TestFileResult] = Field(default_factory=list)
50
+ coverage_before: float = 0.0
51
+ coverage_after: float = 0.0
52
+ coverage_delta: float = 0.0
53
+ rolled_back: bool = False
54
+ duration_seconds: float = 0.0
55
+
56
+
57
+ class RunReport(BaseModel):
58
+ """Final report for a complete Forge Core run."""
59
+
60
+ project_name: str = ""
61
+ project_path: str = ""
62
+ language: str = ""
63
+ framework: str = ""
64
+ started_at: datetime = Field(default_factory=datetime.now)
65
+ completed_at: Optional[datetime] = None
66
+ duration_seconds: float = 0.0
67
+
68
+ # Coverage
69
+ coverage_before: float = 0.0
70
+ coverage_after: float = 0.0
71
+ coverage_delta: float = 0.0
72
+
73
+ # Tests
74
+ total_tests_before: int = 0
75
+ total_tests_after: int = 0
76
+ tests_generated: int = 0
77
+ tests_fixed: int = 0
78
+ test_files_created: list[str] = Field(default_factory=list)
79
+
80
+ # Iterations
81
+ iterations: list[IterationResult] = Field(default_factory=list)
82
+ total_iterations: int = 0
83
+ rollbacks: int = 0
84
+
85
+ # Learning
86
+ patterns_discovered: int = 0
87
+ journeys_mapped: int = 0
88
+ dtos_registered: int = 0
89
+
90
+ # Metadata
91
+ mode: str = "full"
92
+ target_coverage: float = 90.0
93
+ production_files_changed: int = 0 # must always be 0
@@ -0,0 +1,233 @@
1
+ """8-Phase pipeline orchestrator — the heart of Forge Core."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from pathlib import Path
7
+
8
+ from forge_core.ai.prompts import load_learnings, load_prompt
9
+ from forge_core.config import load_config
10
+ from forge_core.core.agent_manager import AgentManager
11
+ from forge_core.core.coverage import run_coverage, run_tests
12
+ from forge_core.core.file_manager import FileManager
13
+ from forge_core.models.config import ForgeConfig, RunMode
14
+ from forge_core.models.dto import DTORegistry
15
+ from forge_core.models.project import ProjectGraph
16
+ from forge_core.models.test_result import CoverageReport, IterationResult, RunReport
17
+ from forge_core.phases import (
18
+ analyze_project,
19
+ audit_tests,
20
+ compile_fix,
21
+ coverage_report,
22
+ detect_stack,
23
+ exclusion_scan,
24
+ fix_broken,
25
+ generate_tests,
26
+ journey_mapping,
27
+ self_learn,
28
+ )
29
+ from forge_core.utils import logger
30
+
31
+
32
+ class Orchestrator:
33
+ """Runs the full 8-phase Forge Core pipeline."""
34
+
35
+ def __init__(self, config: ForgeConfig):
36
+ self.config = config
37
+ self.file_manager = FileManager(config.project_path)
38
+ self.agent_manager = AgentManager()
39
+ self.project_graph = ProjectGraph()
40
+ self.dto_registry = DTORegistry()
41
+ self.report = RunReport(
42
+ project_path=str(config.project_path),
43
+ mode=config.mode.value,
44
+ target_coverage=config.target_coverage,
45
+ )
46
+
47
+ def run(self) -> RunReport:
48
+ """Execute the full pipeline and return the run report."""
49
+ start_time = time.time()
50
+ self.report.started_at = __import__("datetime").datetime.now()
51
+
52
+ logger.banner("Forge Core v2.0", "AI-Powered Backend Test Generation Engine")
53
+
54
+ prompts_dir = Path(self.config.prompts_dir)
55
+
56
+ # Phase -1: Load past learnings
57
+ logger.phase_start("-1", "Loading past learnings")
58
+ learnings = load_learnings(
59
+ self.config.central_agent_path or None,
60
+ self.config.project_path,
61
+ )
62
+ if learnings:
63
+ logger.phase_done("-1", f"Loaded {len(learnings)} chars of learnings")
64
+ else:
65
+ logger.phase_done("-1", "No prior learnings found")
66
+
67
+ # Phase 1: Detect tech stack
68
+ logger.phase_start("1/8", "Detecting tech stack")
69
+ tech_stack = detect_stack.run(
70
+ config=self.config,
71
+ file_manager=self.file_manager,
72
+ prompts_dir=prompts_dir,
73
+ )
74
+ self.project_graph.tech_stack = tech_stack
75
+ self.report.language = tech_stack.language
76
+ self.report.framework = tech_stack.framework
77
+ self.report.project_name = self.config.project_path.name
78
+ logger.phase_done("1/8", f"{tech_stack.language}/{tech_stack.framework} detected")
79
+
80
+ # Phase 1.5: Coverage exclusion scan
81
+ logger.phase_start("1.5/8", "Scanning coverage exclusions")
82
+ exclusions = exclusion_scan.run(
83
+ config=self.config,
84
+ file_manager=self.file_manager,
85
+ tech_stack=tech_stack,
86
+ )
87
+ logger.phase_done("1.5/8", f"{len(exclusions)} exclusions found")
88
+
89
+ # Phase 2: Analyze architecture
90
+ logger.phase_start("2/8", "Analyzing architecture & building project graph")
91
+ self.project_graph = analyze_project.run(
92
+ config=self.config,
93
+ file_manager=self.file_manager,
94
+ prompts_dir=prompts_dir,
95
+ tech_stack=tech_stack,
96
+ learnings=learnings,
97
+ )
98
+ logger.phase_done(
99
+ "2/8",
100
+ f"{len(self.project_graph.modules)} modules, "
101
+ f"{self.project_graph.total_source_files} source files",
102
+ )
103
+
104
+ # Phase 2.5: Journey mapping & DTO registry
105
+ logger.phase_start("2.5/8", "Mapping journeys & building DTO registry")
106
+ self.dto_registry = journey_mapping.run(
107
+ config=self.config,
108
+ file_manager=self.file_manager,
109
+ prompts_dir=prompts_dir,
110
+ project_graph=self.project_graph,
111
+ learnings=learnings,
112
+ )
113
+ journey_count = sum(len(m.journeys) for m in self.project_graph.modules)
114
+ self.report.journeys_mapped = journey_count
115
+ self.report.dtos_registered = self.dto_registry.count
116
+ logger.phase_done(
117
+ "2.5/8",
118
+ f"{journey_count} journeys mapped, {self.dto_registry.count} DTOs registered",
119
+ )
120
+
121
+ # Stop here for analyze-only modes
122
+ if self.config.mode == RunMode.ANALYZE_ONLY:
123
+ logger.success("Analysis complete (analyze-only mode)")
124
+ self.report.duration_seconds = time.time() - start_time
125
+ return self.report
126
+
127
+ # Phase 3: Audit existing tests
128
+ logger.phase_start("3/8", "Auditing existing tests")
129
+ baseline = audit_tests.run(
130
+ config=self.config,
131
+ file_manager=self.file_manager,
132
+ prompts_dir=prompts_dir,
133
+ project_graph=self.project_graph,
134
+ )
135
+ self.report.coverage_before = baseline.line_coverage
136
+ self.report.total_tests_before = baseline.total_tests
137
+ logger.phase_done(
138
+ "3/8",
139
+ f"Baseline: {baseline.line_coverage:.1f}% coverage, "
140
+ f"{baseline.total_tests} tests ({baseline.tests_failed} failing)",
141
+ )
142
+
143
+ if self.config.mode == RunMode.ANALYZE_REVIEW:
144
+ logger.success("Analysis + review complete")
145
+ self.report.duration_seconds = time.time() - start_time
146
+ return self.report
147
+
148
+ # Phase 4: Fix broken tests
149
+ if baseline.tests_failed > 0:
150
+ logger.phase_start("4/8", "Fixing broken tests")
151
+ fixed_count = fix_broken.run(
152
+ config=self.config,
153
+ file_manager=self.file_manager,
154
+ prompts_dir=prompts_dir,
155
+ project_graph=self.project_graph,
156
+ baseline=baseline,
157
+ learnings=learnings,
158
+ )
159
+ self.report.tests_fixed = fixed_count
160
+ logger.phase_done("4/8", f"Fixed {fixed_count} broken tests")
161
+ else:
162
+ logger.phase_skip("4/8", "No broken tests found")
163
+
164
+ # Get post-fix coverage as new baseline
165
+ post_fix = run_coverage(
166
+ self.config.project_path,
167
+ self.project_graph.tech_stack.coverage_command,
168
+ )
169
+ best_coverage = max(baseline.line_coverage, post_fix.line_coverage)
170
+
171
+ # Phase 5: Iterative test generation
172
+ logger.phase_start("5/8", "Generating tests (journey-weighted)")
173
+ generation_result = generate_tests.run(
174
+ config=self.config,
175
+ file_manager=self.file_manager,
176
+ prompts_dir=prompts_dir,
177
+ project_graph=self.project_graph,
178
+ dto_registry=self.dto_registry,
179
+ agent_manager=self.agent_manager,
180
+ baseline_coverage=best_coverage,
181
+ learnings=learnings,
182
+ )
183
+ self.report.iterations = generation_result.iterations
184
+ self.report.total_iterations = len(generation_result.iterations)
185
+ self.report.rollbacks = sum(1 for i in generation_result.iterations if i.rolled_back)
186
+
187
+ # Phase 6: Final coverage report
188
+ logger.phase_start("6/8", "Generating final coverage report")
189
+ final = coverage_report.run(
190
+ config=self.config,
191
+ project_graph=self.project_graph,
192
+ )
193
+ self.report.coverage_after = final.line_coverage
194
+ self.report.coverage_delta = final.line_coverage - self.report.coverage_before
195
+ self.report.total_tests_after = final.total_tests
196
+ self.report.tests_generated = final.total_tests - self.report.total_tests_before
197
+ logger.phase_done("6/8", f"Final coverage: {final.line_coverage:.1f}%")
198
+
199
+ logger.coverage_table(
200
+ self.report.coverage_before,
201
+ self.report.coverage_after,
202
+ self.report.total_tests_before,
203
+ self.report.total_tests_after,
204
+ )
205
+
206
+ # Phase 7: Self-learn
207
+ logger.phase_start("7/8", "Self-learning")
208
+ patterns = self_learn.run(
209
+ config=self.config,
210
+ file_manager=self.file_manager,
211
+ prompts_dir=prompts_dir,
212
+ project_graph=self.project_graph,
213
+ report=self.report,
214
+ learnings=learnings,
215
+ )
216
+ self.report.patterns_discovered = patterns
217
+ logger.phase_done("7/8", f"{patterns} new patterns recorded")
218
+
219
+ # Finalize
220
+ self.report.duration_seconds = time.time() - start_time
221
+ self.report.completed_at = __import__("datetime").datetime.now()
222
+ self.report.production_files_changed = 0 # always 0
223
+
224
+ mins = self.report.duration_seconds / 60
225
+ logger.banner(
226
+ "✅ Forge Core Complete",
227
+ f"{self.report.coverage_before:.1f}% → {self.report.coverage_after:.1f}% "
228
+ f"in {mins:.1f} minutes | "
229
+ f"+{self.report.tests_generated} tests | "
230
+ f"{self.report.patterns_discovered} patterns learned",
231
+ )
232
+
233
+ return self.report
File without changes
@@ -0,0 +1,149 @@
1
+ """Phase 2 — Analyze project architecture and build project graph."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from forge_core.ai.prompts import build_file_context, load_prompt
8
+ from forge_core.ai.provider import complete
9
+ from forge_core.core.file_manager import FileManager
10
+ from forge_core.models.config import ForgeConfig
11
+ from forge_core.models.project import Component, Layer, Module, ProjectGraph, TechStack
12
+ from forge_core.utils import logger
13
+ from forge_core.utils.tokens import split_for_context
14
+
15
+ import json
16
+
17
+
18
+ def run(
19
+ config: ForgeConfig,
20
+ file_manager: FileManager,
21
+ prompts_dir: Path,
22
+ tech_stack: TechStack,
23
+ learnings: str = "",
24
+ ) -> ProjectGraph:
25
+ """Analyze the project and build the 4-Level DAG."""
26
+ # Read all source files
27
+ source_root = tech_stack.source_root or "src"
28
+ source_files = file_manager.read_files(f"{source_root}/**/*")
29
+ test_files = file_manager.read_files(f"{tech_stack.test_root or 'src/test'}/**/*")
30
+
31
+ # Filter to code files only
32
+ code_extensions = _get_code_extensions(tech_stack.language)
33
+ source_files = {
34
+ k: v for k, v in source_files.items()
35
+ if any(k.endswith(ext) for ext in code_extensions)
36
+ }
37
+
38
+ graph = ProjectGraph(
39
+ name=config.project_path.name,
40
+ root_path=str(config.project_path),
41
+ tech_stack=tech_stack,
42
+ total_source_files=len(source_files),
43
+ total_test_files=len(test_files),
44
+ )
45
+
46
+ # Split files into batches that fit in context
47
+ batches = split_for_context(source_files, max_tokens=80_000, model=config.ai.model)
48
+
49
+ prompt = load_prompt(prompts_dir, "analyze-project")
50
+ system_prompt = prompt or (
51
+ "You are a software architect. Analyze this codebase and identify:\n"
52
+ "1. Modules/packages\n2. Layers (controllers, services, adapters, models)\n"
53
+ "3. Key components per layer\n4. Dependencies between components\n"
54
+ "Return JSON with modules, layers, and components."
55
+ )
56
+
57
+ if learnings:
58
+ system_prompt += f"\n\nPast learnings:\n{learnings[:2000]}"
59
+
60
+ # Process each batch
61
+ all_modules: dict[str, Module] = {}
62
+ for i, batch in enumerate(batches):
63
+ logger.info(f"Analyzing batch {i + 1}/{len(batches)} ({len(batch)} files)")
64
+ file_context = build_file_context(batch)
65
+
66
+ response = complete(
67
+ config=config.ai,
68
+ system_prompt=system_prompt,
69
+ user_prompt=(
70
+ f"Analyze these source files and return the architecture as JSON.\n\n"
71
+ f"Project: {config.project_path.name}\n"
72
+ f"Language: {tech_stack.language}\n"
73
+ f"Framework: {tech_stack.framework}\n\n"
74
+ f"{file_context}"
75
+ ),
76
+ json_mode=True,
77
+ )
78
+
79
+ _parse_architecture_response(response, all_modules, batch)
80
+
81
+ graph.modules = list(all_modules.values())
82
+ return graph
83
+
84
+
85
+ def _parse_architecture_response(
86
+ response: str, modules: dict[str, Module], files: dict[str, str]
87
+ ) -> None:
88
+ """Parse AI's architecture analysis response and merge into modules dict."""
89
+ try:
90
+ data = json.loads(response)
91
+ except json.JSONDecodeError:
92
+ logger.warn("Failed to parse architecture JSON — using file-based fallback")
93
+ _fallback_from_files(modules, files)
94
+ return
95
+
96
+ for mod_data in data.get("modules", [data] if "layers" in data else []):
97
+ mod_name = mod_data.get("name", "main")
98
+ if mod_name not in modules:
99
+ modules[mod_name] = Module(name=mod_name, path=mod_data.get("path", ""))
100
+
101
+ mod = modules[mod_name]
102
+ for layer_data in mod_data.get("layers", []):
103
+ layer = Layer(name=layer_data.get("name", ""))
104
+ for comp_data in layer_data.get("components", []):
105
+ layer.components.append(
106
+ Component(
107
+ name=comp_data.get("name", ""),
108
+ file_path=comp_data.get("file_path", ""),
109
+ layer=layer.name,
110
+ dependencies=comp_data.get("dependencies", []),
111
+ )
112
+ )
113
+ mod.layers.append(layer)
114
+
115
+
116
+ def _fallback_from_files(modules: dict[str, Module], files: dict[str, str]) -> None:
117
+ """Build a basic module structure from file paths when AI parsing fails."""
118
+ mod = modules.setdefault("main", Module(name="main", path="src"))
119
+ layers: dict[str, Layer] = {}
120
+
121
+ for path in files:
122
+ parts = Path(path).parts
123
+ layer_name = parts[2] if len(parts) > 2 else "root"
124
+ if layer_name not in layers:
125
+ layers[layer_name] = Layer(name=layer_name)
126
+ layers[layer_name].components.append(
127
+ Component(name=Path(path).stem, file_path=path, layer=layer_name)
128
+ )
129
+
130
+ mod.layers = list(layers.values())
131
+
132
+
133
+ def _get_code_extensions(language: str) -> list[str]:
134
+ """Get relevant file extensions for a language."""
135
+ ext_map = {
136
+ "kotlin": [".kt", ".kts"],
137
+ "java": [".java"],
138
+ "python": [".py"],
139
+ "go": [".go"],
140
+ "typescript": [".ts", ".tsx"],
141
+ "javascript": [".js", ".jsx"],
142
+ "rust": [".rs"],
143
+ "csharp": [".cs"],
144
+ "c#": [".cs"],
145
+ "ruby": [".rb"],
146
+ "php": [".php"],
147
+ "c++": [".cpp", ".hpp", ".cc", ".h"],
148
+ }
149
+ return ext_map.get(language.lower(), [".py", ".java", ".kt", ".go", ".ts", ".js"])
@@ -0,0 +1,35 @@
1
+ """Phase 3 — Audit existing tests and establish baseline."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from forge_core.core.coverage import run_coverage
8
+ from forge_core.core.file_manager import FileManager
9
+ from forge_core.models.config import ForgeConfig
10
+ from forge_core.models.project import ProjectGraph
11
+ from forge_core.models.test_result import CoverageReport
12
+ from forge_core.utils import logger
13
+
14
+
15
+ def run(
16
+ config: ForgeConfig,
17
+ file_manager: FileManager,
18
+ prompts_dir: Path,
19
+ project_graph: ProjectGraph,
20
+ ) -> CoverageReport:
21
+ """Audit existing tests and get baseline coverage."""
22
+ tech = project_graph.tech_stack
23
+
24
+ if not tech.coverage_command:
25
+ logger.warn("No coverage command detected — using test command")
26
+ tech.coverage_command = tech.test_command
27
+
28
+ if not tech.coverage_command:
29
+ logger.warn("No test/coverage command found")
30
+ return CoverageReport()
31
+
32
+ # Run existing tests and get baseline coverage
33
+ report = run_coverage(config.project_path, tech.coverage_command)
34
+
35
+ return report
@@ -0,0 +1,79 @@
1
+ """Phase 5b — Auto compile-fix loop."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from forge_core.ai.provider import complete
8
+ from forge_core.core.file_manager import FileManager
9
+ from forge_core.models.config import ForgeConfig
10
+ from forge_core.utils import logger
11
+ from forge_core.utils.shell import run as shell_run
12
+
13
+ import json
14
+
15
+
16
+ def fix_compilation_errors(
17
+ config: ForgeConfig,
18
+ file_manager: FileManager,
19
+ test_command: str,
20
+ max_attempts: int = 3,
21
+ ) -> bool:
22
+ """Attempt to fix compilation errors in generated tests.
23
+
24
+ Returns True if tests compile after fixes.
25
+ """
26
+ for attempt in range(1, max_attempts + 1):
27
+ result = shell_run(test_command, cwd=config.project_path, timeout=300)
28
+
29
+ if result.success:
30
+ return True
31
+
32
+ # Check if it's a compilation error (not a test failure)
33
+ if not _is_compile_error(result.output):
34
+ return True # Tests compiled but some failed — that's okay
35
+
36
+ logger.info(f"Compile-fix attempt {attempt}/{max_attempts}")
37
+
38
+ # Ask AI to fix
39
+ response = complete(
40
+ config=config.ai,
41
+ system_prompt=(
42
+ "Fix the compilation errors in these test files. "
43
+ "Return JSON with fixed_files: [{path, content}]. "
44
+ "Only fix imports, types, and syntax — don't change test logic."
45
+ ),
46
+ user_prompt=f"Compilation errors:\n```\n{result.output[:4000]}\n```",
47
+ json_mode=True,
48
+ )
49
+
50
+ try:
51
+ data = json.loads(response)
52
+ for fix in data.get("fixed_files", []):
53
+ path = fix.get("path", "")
54
+ content = fix.get("content", "")
55
+ if path and content:
56
+ file_manager.write_file(path, content)
57
+ except json.JSONDecodeError:
58
+ logger.warn("Failed to parse compile-fix response")
59
+ continue
60
+
61
+ return False
62
+
63
+
64
+ def _is_compile_error(output: str) -> bool:
65
+ """Detect if the output contains compilation errors (vs test failures)."""
66
+ compile_indicators = [
67
+ "Compilation failed",
68
+ "COMPILE ERROR",
69
+ "error: cannot find symbol",
70
+ "Unresolved reference",
71
+ "SyntaxError",
72
+ "IndentationError",
73
+ "ImportError",
74
+ "ModuleNotFoundError",
75
+ "error TS",
76
+ "cannot resolve",
77
+ "does not exist",
78
+ ]
79
+ return any(indicator.lower() in output.lower() for indicator in compile_indicators)
@@ -0,0 +1,19 @@
1
+ """Phase 6 — Generate final coverage report."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from forge_core.core.coverage import run_coverage
6
+ from forge_core.models.config import ForgeConfig
7
+ from forge_core.models.project import ProjectGraph
8
+ from forge_core.models.test_result import CoverageReport
9
+
10
+
11
+ def run(config: ForgeConfig, project_graph: ProjectGraph) -> CoverageReport:
12
+ """Run the full test suite and generate the final coverage report."""
13
+ tech = project_graph.tech_stack
14
+ command = tech.coverage_command or tech.test_command
15
+
16
+ if not command:
17
+ return CoverageReport()
18
+
19
+ return run_coverage(config.project_path, command)