codexa 0.4.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.
- codexa-0.4.0.dist-info/METADATA +650 -0
- codexa-0.4.0.dist-info/RECORD +189 -0
- codexa-0.4.0.dist-info/WHEEL +5 -0
- codexa-0.4.0.dist-info/entry_points.txt +2 -0
- codexa-0.4.0.dist-info/licenses/LICENSE +21 -0
- codexa-0.4.0.dist-info/top_level.txt +1 -0
- semantic_code_intelligence/__init__.py +5 -0
- semantic_code_intelligence/analysis/__init__.py +21 -0
- semantic_code_intelligence/analysis/ai_features.py +351 -0
- semantic_code_intelligence/bridge/__init__.py +28 -0
- semantic_code_intelligence/bridge/context_provider.py +245 -0
- semantic_code_intelligence/bridge/protocol.py +167 -0
- semantic_code_intelligence/bridge/server.py +348 -0
- semantic_code_intelligence/bridge/vscode.py +271 -0
- semantic_code_intelligence/ci/__init__.py +13 -0
- semantic_code_intelligence/ci/hooks.py +98 -0
- semantic_code_intelligence/ci/hotspots.py +272 -0
- semantic_code_intelligence/ci/impact.py +246 -0
- semantic_code_intelligence/ci/metrics.py +591 -0
- semantic_code_intelligence/ci/pr.py +412 -0
- semantic_code_intelligence/ci/quality.py +557 -0
- semantic_code_intelligence/ci/templates.py +164 -0
- semantic_code_intelligence/ci/trace.py +224 -0
- semantic_code_intelligence/cli/__init__.py +0 -0
- semantic_code_intelligence/cli/commands/__init__.py +0 -0
- semantic_code_intelligence/cli/commands/ask_cmd.py +153 -0
- semantic_code_intelligence/cli/commands/benchmark_cmd.py +303 -0
- semantic_code_intelligence/cli/commands/chat_cmd.py +252 -0
- semantic_code_intelligence/cli/commands/ci_gen_cmd.py +74 -0
- semantic_code_intelligence/cli/commands/context_cmd.py +120 -0
- semantic_code_intelligence/cli/commands/cross_refactor_cmd.py +113 -0
- semantic_code_intelligence/cli/commands/deps_cmd.py +91 -0
- semantic_code_intelligence/cli/commands/docs_cmd.py +101 -0
- semantic_code_intelligence/cli/commands/doctor_cmd.py +147 -0
- semantic_code_intelligence/cli/commands/evolve_cmd.py +171 -0
- semantic_code_intelligence/cli/commands/explain_cmd.py +112 -0
- semantic_code_intelligence/cli/commands/gate_cmd.py +135 -0
- semantic_code_intelligence/cli/commands/grep_cmd.py +234 -0
- semantic_code_intelligence/cli/commands/hotspots_cmd.py +119 -0
- semantic_code_intelligence/cli/commands/impact_cmd.py +131 -0
- semantic_code_intelligence/cli/commands/index_cmd.py +138 -0
- semantic_code_intelligence/cli/commands/init_cmd.py +152 -0
- semantic_code_intelligence/cli/commands/investigate_cmd.py +163 -0
- semantic_code_intelligence/cli/commands/languages_cmd.py +101 -0
- semantic_code_intelligence/cli/commands/lsp_cmd.py +49 -0
- semantic_code_intelligence/cli/commands/mcp_cmd.py +50 -0
- semantic_code_intelligence/cli/commands/metrics_cmd.py +264 -0
- semantic_code_intelligence/cli/commands/models_cmd.py +157 -0
- semantic_code_intelligence/cli/commands/plugin_cmd.py +275 -0
- semantic_code_intelligence/cli/commands/pr_summary_cmd.py +178 -0
- semantic_code_intelligence/cli/commands/quality_cmd.py +208 -0
- semantic_code_intelligence/cli/commands/refactor_cmd.py +103 -0
- semantic_code_intelligence/cli/commands/review_cmd.py +88 -0
- semantic_code_intelligence/cli/commands/search_cmd.py +236 -0
- semantic_code_intelligence/cli/commands/serve_cmd.py +117 -0
- semantic_code_intelligence/cli/commands/suggest_cmd.py +100 -0
- semantic_code_intelligence/cli/commands/summary_cmd.py +78 -0
- semantic_code_intelligence/cli/commands/tool_cmd.py +282 -0
- semantic_code_intelligence/cli/commands/trace_cmd.py +123 -0
- semantic_code_intelligence/cli/commands/tui_cmd.py +58 -0
- semantic_code_intelligence/cli/commands/viz_cmd.py +127 -0
- semantic_code_intelligence/cli/commands/watch_cmd.py +72 -0
- semantic_code_intelligence/cli/commands/web_cmd.py +61 -0
- semantic_code_intelligence/cli/commands/workspace_cmd.py +250 -0
- semantic_code_intelligence/cli/main.py +65 -0
- semantic_code_intelligence/cli/router.py +92 -0
- semantic_code_intelligence/config/__init__.py +0 -0
- semantic_code_intelligence/config/settings.py +260 -0
- semantic_code_intelligence/context/__init__.py +19 -0
- semantic_code_intelligence/context/engine.py +429 -0
- semantic_code_intelligence/context/memory.py +253 -0
- semantic_code_intelligence/daemon/__init__.py +1 -0
- semantic_code_intelligence/daemon/watcher.py +515 -0
- semantic_code_intelligence/docs/__init__.py +1080 -0
- semantic_code_intelligence/embeddings/__init__.py +0 -0
- semantic_code_intelligence/embeddings/enhanced.py +131 -0
- semantic_code_intelligence/embeddings/generator.py +149 -0
- semantic_code_intelligence/embeddings/model_registry.py +100 -0
- semantic_code_intelligence/evolution/__init__.py +1 -0
- semantic_code_intelligence/evolution/budget_guard.py +111 -0
- semantic_code_intelligence/evolution/commit_manager.py +88 -0
- semantic_code_intelligence/evolution/context_builder.py +131 -0
- semantic_code_intelligence/evolution/engine.py +249 -0
- semantic_code_intelligence/evolution/patch_generator.py +229 -0
- semantic_code_intelligence/evolution/task_selector.py +214 -0
- semantic_code_intelligence/evolution/test_runner.py +111 -0
- semantic_code_intelligence/indexing/__init__.py +0 -0
- semantic_code_intelligence/indexing/chunker.py +174 -0
- semantic_code_intelligence/indexing/parallel.py +86 -0
- semantic_code_intelligence/indexing/scanner.py +146 -0
- semantic_code_intelligence/indexing/semantic_chunker.py +337 -0
- semantic_code_intelligence/llm/__init__.py +62 -0
- semantic_code_intelligence/llm/cache.py +219 -0
- semantic_code_intelligence/llm/cached_provider.py +145 -0
- semantic_code_intelligence/llm/conversation.py +190 -0
- semantic_code_intelligence/llm/cross_refactor.py +272 -0
- semantic_code_intelligence/llm/investigation.py +274 -0
- semantic_code_intelligence/llm/mock_provider.py +77 -0
- semantic_code_intelligence/llm/ollama_provider.py +122 -0
- semantic_code_intelligence/llm/openai_provider.py +100 -0
- semantic_code_intelligence/llm/provider.py +92 -0
- semantic_code_intelligence/llm/rate_limiter.py +164 -0
- semantic_code_intelligence/llm/reasoning.py +438 -0
- semantic_code_intelligence/llm/safety.py +110 -0
- semantic_code_intelligence/llm/streaming.py +251 -0
- semantic_code_intelligence/lsp/__init__.py +609 -0
- semantic_code_intelligence/mcp/__init__.py +393 -0
- semantic_code_intelligence/parsing/__init__.py +19 -0
- semantic_code_intelligence/parsing/parser.py +375 -0
- semantic_code_intelligence/plugins/__init__.py +255 -0
- semantic_code_intelligence/plugins/examples/__init__.py +1 -0
- semantic_code_intelligence/plugins/examples/code_quality.py +73 -0
- semantic_code_intelligence/plugins/examples/search_annotator.py +56 -0
- semantic_code_intelligence/scalability/__init__.py +205 -0
- semantic_code_intelligence/search/__init__.py +0 -0
- semantic_code_intelligence/search/formatter.py +123 -0
- semantic_code_intelligence/search/grep.py +361 -0
- semantic_code_intelligence/search/hybrid_search.py +170 -0
- semantic_code_intelligence/search/keyword_search.py +311 -0
- semantic_code_intelligence/search/section_expander.py +103 -0
- semantic_code_intelligence/services/__init__.py +0 -0
- semantic_code_intelligence/services/indexing_service.py +630 -0
- semantic_code_intelligence/services/search_service.py +269 -0
- semantic_code_intelligence/storage/__init__.py +0 -0
- semantic_code_intelligence/storage/chunk_hash_store.py +86 -0
- semantic_code_intelligence/storage/hash_store.py +66 -0
- semantic_code_intelligence/storage/index_manifest.py +85 -0
- semantic_code_intelligence/storage/index_stats.py +138 -0
- semantic_code_intelligence/storage/query_history.py +160 -0
- semantic_code_intelligence/storage/symbol_registry.py +209 -0
- semantic_code_intelligence/storage/vector_store.py +297 -0
- semantic_code_intelligence/tests/__init__.py +0 -0
- semantic_code_intelligence/tests/test_ai_features.py +351 -0
- semantic_code_intelligence/tests/test_chunker.py +119 -0
- semantic_code_intelligence/tests/test_cli.py +188 -0
- semantic_code_intelligence/tests/test_config.py +154 -0
- semantic_code_intelligence/tests/test_context.py +381 -0
- semantic_code_intelligence/tests/test_embeddings.py +73 -0
- semantic_code_intelligence/tests/test_endtoend.py +1142 -0
- semantic_code_intelligence/tests/test_enhanced_embeddings.py +92 -0
- semantic_code_intelligence/tests/test_hash_store.py +79 -0
- semantic_code_intelligence/tests/test_logging.py +55 -0
- semantic_code_intelligence/tests/test_new_cli.py +138 -0
- semantic_code_intelligence/tests/test_parser.py +495 -0
- semantic_code_intelligence/tests/test_phase10.py +355 -0
- semantic_code_intelligence/tests/test_phase11.py +593 -0
- semantic_code_intelligence/tests/test_phase12.py +375 -0
- semantic_code_intelligence/tests/test_phase13.py +663 -0
- semantic_code_intelligence/tests/test_phase14.py +568 -0
- semantic_code_intelligence/tests/test_phase15.py +814 -0
- semantic_code_intelligence/tests/test_phase16.py +792 -0
- semantic_code_intelligence/tests/test_phase17.py +815 -0
- semantic_code_intelligence/tests/test_phase18.py +934 -0
- semantic_code_intelligence/tests/test_phase19.py +986 -0
- semantic_code_intelligence/tests/test_phase20.py +2753 -0
- semantic_code_intelligence/tests/test_phase20b.py +2058 -0
- semantic_code_intelligence/tests/test_phase20c.py +962 -0
- semantic_code_intelligence/tests/test_phase21.py +428 -0
- semantic_code_intelligence/tests/test_phase22.py +799 -0
- semantic_code_intelligence/tests/test_phase23.py +783 -0
- semantic_code_intelligence/tests/test_phase24.py +715 -0
- semantic_code_intelligence/tests/test_phase25.py +496 -0
- semantic_code_intelligence/tests/test_phase26.py +251 -0
- semantic_code_intelligence/tests/test_phase27.py +531 -0
- semantic_code_intelligence/tests/test_phase8.py +592 -0
- semantic_code_intelligence/tests/test_phase9.py +643 -0
- semantic_code_intelligence/tests/test_plugins.py +293 -0
- semantic_code_intelligence/tests/test_priority_features.py +727 -0
- semantic_code_intelligence/tests/test_router.py +41 -0
- semantic_code_intelligence/tests/test_scalability.py +138 -0
- semantic_code_intelligence/tests/test_scanner.py +125 -0
- semantic_code_intelligence/tests/test_search.py +160 -0
- semantic_code_intelligence/tests/test_semantic_chunker.py +255 -0
- semantic_code_intelligence/tests/test_tools.py +182 -0
- semantic_code_intelligence/tests/test_vector_store.py +151 -0
- semantic_code_intelligence/tests/test_watcher.py +211 -0
- semantic_code_intelligence/tools/__init__.py +442 -0
- semantic_code_intelligence/tools/executor.py +232 -0
- semantic_code_intelligence/tools/protocol.py +200 -0
- semantic_code_intelligence/tui/__init__.py +454 -0
- semantic_code_intelligence/utils/__init__.py +0 -0
- semantic_code_intelligence/utils/logging.py +112 -0
- semantic_code_intelligence/version.py +3 -0
- semantic_code_intelligence/web/__init__.py +11 -0
- semantic_code_intelligence/web/api.py +289 -0
- semantic_code_intelligence/web/server.py +397 -0
- semantic_code_intelligence/web/ui.py +659 -0
- semantic_code_intelligence/web/visualize.py +226 -0
- semantic_code_intelligence/workspace/__init__.py +427 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Context builder — constructs minimal LLM prompt context.
|
|
2
|
+
|
|
3
|
+
The builder gathers *only* the information the LLM needs to generate a
|
|
4
|
+
patch: the task description, the target file contents (or relevant
|
|
5
|
+
excerpts), the git diff, and failing test output. The assembled context
|
|
6
|
+
is capped at a configurable token target (default 2 000 tokens ≈ 8 000
|
|
7
|
+
chars) to keep LLM costs low.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from semantic_code_intelligence.evolution.budget_guard import BudgetGuard
|
|
15
|
+
from semantic_code_intelligence.evolution.commit_manager import CommitManager
|
|
16
|
+
from semantic_code_intelligence.evolution.task_selector import EvolutionTask
|
|
17
|
+
from semantic_code_intelligence.utils.logging import get_logger
|
|
18
|
+
|
|
19
|
+
logger = get_logger("evolution.context_builder")
|
|
20
|
+
|
|
21
|
+
# Rough approximation: 1 token ≈ 4 characters
|
|
22
|
+
_CHARS_PER_TOKEN = 4
|
|
23
|
+
_DEFAULT_MAX_TOKENS = 2000
|
|
24
|
+
|
|
25
|
+
# System prompt used for every evolution call
|
|
26
|
+
SYSTEM_PROMPT = (
|
|
27
|
+
"You are a senior software engineer improving the CodexA codebase. "
|
|
28
|
+
"Your job is to make a SMALL, SAFE improvement. Rules:\n"
|
|
29
|
+
"- Change at most 3 files and 200 lines.\n"
|
|
30
|
+
"- Output ONLY a unified diff (no explanation before or after).\n"
|
|
31
|
+
"- Do NOT add new dependencies.\n"
|
|
32
|
+
"- Do NOT rewrite subsystems or change architecture.\n"
|
|
33
|
+
"- Preserve all existing tests.\n"
|
|
34
|
+
"- Use full type hints.\n"
|
|
35
|
+
"- Keep it simple."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ContextBuilder:
|
|
40
|
+
"""Assembles a minimal prompt context for the patch generator."""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
project_root: Path,
|
|
45
|
+
commit_manager: CommitManager,
|
|
46
|
+
max_context_tokens: int = _DEFAULT_MAX_TOKENS,
|
|
47
|
+
) -> None:
|
|
48
|
+
self._root = project_root.resolve()
|
|
49
|
+
self._git = commit_manager
|
|
50
|
+
self._max_chars = max_context_tokens * _CHARS_PER_TOKEN
|
|
51
|
+
|
|
52
|
+
def build(self, task: EvolutionTask) -> str:
|
|
53
|
+
"""Build the user-message portion of the LLM prompt.
|
|
54
|
+
|
|
55
|
+
Sections (included in priority order until budget exhausted):
|
|
56
|
+
1. Task description (always included)
|
|
57
|
+
2. Target file contents (trimmed to budget)
|
|
58
|
+
3. Git diff (if any)
|
|
59
|
+
4. Failing test output (if present in task context_hint)
|
|
60
|
+
"""
|
|
61
|
+
parts: list[str] = []
|
|
62
|
+
budget = self._max_chars
|
|
63
|
+
|
|
64
|
+
# 1. Task description
|
|
65
|
+
header = self._task_section(task)
|
|
66
|
+
parts.append(header)
|
|
67
|
+
budget -= len(header)
|
|
68
|
+
|
|
69
|
+
# 2. Target file contents
|
|
70
|
+
for rel_path in task.target_files[:3]:
|
|
71
|
+
if budget <= 200:
|
|
72
|
+
break
|
|
73
|
+
section = self._file_section(rel_path, budget)
|
|
74
|
+
if section:
|
|
75
|
+
parts.append(section)
|
|
76
|
+
budget -= len(section)
|
|
77
|
+
|
|
78
|
+
# 3. Git diff
|
|
79
|
+
if budget > 200:
|
|
80
|
+
diff = self._git.git_diff()
|
|
81
|
+
if diff.strip():
|
|
82
|
+
diff_section = f"### Current git diff\n```diff\n{_truncate(diff, budget - 60)}\n```"
|
|
83
|
+
parts.append(diff_section)
|
|
84
|
+
budget -= len(diff_section)
|
|
85
|
+
|
|
86
|
+
# 4. Context hint (failing test output, etc.)
|
|
87
|
+
if budget > 200 and task.context_hint:
|
|
88
|
+
hint = f"### Additional context\n```\n{_truncate(task.context_hint, budget - 60)}\n```"
|
|
89
|
+
parts.append(hint)
|
|
90
|
+
|
|
91
|
+
return "\n\n".join(parts)
|
|
92
|
+
|
|
93
|
+
def estimate_tokens(self, text: str) -> int:
|
|
94
|
+
"""Rough token estimate for a text string."""
|
|
95
|
+
return max(1, len(text) // _CHARS_PER_TOKEN)
|
|
96
|
+
|
|
97
|
+
# ------------------------------------------------------------------ #
|
|
98
|
+
# Section builders
|
|
99
|
+
# ------------------------------------------------------------------ #
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def _task_section(task: EvolutionTask) -> str:
|
|
103
|
+
lines = [
|
|
104
|
+
f"### Task: {task.category}",
|
|
105
|
+
task.description,
|
|
106
|
+
]
|
|
107
|
+
if task.target_files:
|
|
108
|
+
lines.append(f"Target files: {', '.join(task.target_files)}")
|
|
109
|
+
lines.append(
|
|
110
|
+
"\nProduce a unified diff that implements this improvement."
|
|
111
|
+
)
|
|
112
|
+
return "\n".join(lines)
|
|
113
|
+
|
|
114
|
+
def _file_section(self, rel_path: str, budget: int) -> str | None:
|
|
115
|
+
"""Read a target file and return a fenced code block."""
|
|
116
|
+
full = self._root / rel_path
|
|
117
|
+
if not full.exists():
|
|
118
|
+
return None
|
|
119
|
+
try:
|
|
120
|
+
content = full.read_text(encoding="utf-8", errors="replace")
|
|
121
|
+
except OSError:
|
|
122
|
+
return None
|
|
123
|
+
content = _truncate(content, budget - 80)
|
|
124
|
+
return f"### File: {rel_path}\n```python\n{content}\n```"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _truncate(text: str, max_chars: int) -> str:
|
|
128
|
+
"""Truncate *text* to at most *max_chars*, appending '…' if trimmed."""
|
|
129
|
+
if len(text) <= max_chars:
|
|
130
|
+
return text
|
|
131
|
+
return text[:max_chars] + "\n... (truncated)"
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Evolution engine — orchestrates the self-improving development loop.
|
|
2
|
+
|
|
3
|
+
Each iteration:
|
|
4
|
+
1. Run tests to get baseline.
|
|
5
|
+
2. Select a task (fix tests → type hints → error handling → dedup → optimise).
|
|
6
|
+
3. Build minimal LLM context.
|
|
7
|
+
4. Generate + apply patch via LLM.
|
|
8
|
+
5. Run tests again.
|
|
9
|
+
6. If tests pass → commit. If tests fail → revert.
|
|
10
|
+
7. Repeat until budget exhausted.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import time
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from semantic_code_intelligence.evolution.budget_guard import BudgetGuard
|
|
22
|
+
from semantic_code_intelligence.evolution.commit_manager import CommitManager
|
|
23
|
+
from semantic_code_intelligence.evolution.context_builder import ContextBuilder
|
|
24
|
+
from semantic_code_intelligence.evolution.patch_generator import PatchGenerator, PatchResult
|
|
25
|
+
from semantic_code_intelligence.evolution.task_selector import EvolutionTask, TaskSelector
|
|
26
|
+
from semantic_code_intelligence.evolution.test_runner import TestResult, TestRunner
|
|
27
|
+
from semantic_code_intelligence.llm.provider import LLMProvider
|
|
28
|
+
from semantic_code_intelligence.utils.logging import get_logger
|
|
29
|
+
|
|
30
|
+
logger = get_logger("evolution.engine")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ------------------------------------------------------------------ #
|
|
34
|
+
# Result dataclasses
|
|
35
|
+
# ------------------------------------------------------------------ #
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class IterationRecord:
|
|
40
|
+
"""Record of a single evolution iteration."""
|
|
41
|
+
|
|
42
|
+
iteration: int = 0
|
|
43
|
+
task_category: str = ""
|
|
44
|
+
task_description: str = ""
|
|
45
|
+
target_files: list[str] = field(default_factory=list)
|
|
46
|
+
patch_lines_changed: int = 0
|
|
47
|
+
tests_before: int = 0
|
|
48
|
+
tests_after: int = 0
|
|
49
|
+
committed: bool = False
|
|
50
|
+
commit_sha: str = ""
|
|
51
|
+
reverted: bool = False
|
|
52
|
+
error: str = ""
|
|
53
|
+
duration_seconds: float = 0.0
|
|
54
|
+
|
|
55
|
+
def to_dict(self) -> dict[str, Any]:
|
|
56
|
+
"""Serialise the iteration record to a plain dictionary."""
|
|
57
|
+
return {
|
|
58
|
+
"iteration": self.iteration,
|
|
59
|
+
"task_category": self.task_category,
|
|
60
|
+
"task_description": self.task_description,
|
|
61
|
+
"target_files": self.target_files,
|
|
62
|
+
"patch_lines_changed": self.patch_lines_changed,
|
|
63
|
+
"tests_before": self.tests_before,
|
|
64
|
+
"tests_after": self.tests_after,
|
|
65
|
+
"committed": self.committed,
|
|
66
|
+
"commit_sha": self.commit_sha,
|
|
67
|
+
"reverted": self.reverted,
|
|
68
|
+
"error": self.error,
|
|
69
|
+
"duration_seconds": round(self.duration_seconds, 2),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class EvolutionResult:
|
|
75
|
+
"""Aggregate result of an evolution run."""
|
|
76
|
+
|
|
77
|
+
iterations_completed: int = 0
|
|
78
|
+
commits: list[str] = field(default_factory=list)
|
|
79
|
+
reverts: int = 0
|
|
80
|
+
stop_reason: str = ""
|
|
81
|
+
budget_summary: dict[str, object] = field(default_factory=dict)
|
|
82
|
+
history: list[IterationRecord] = field(default_factory=list)
|
|
83
|
+
|
|
84
|
+
def to_dict(self) -> dict[str, Any]:
|
|
85
|
+
"""Serialise the evolution result to a plain dictionary."""
|
|
86
|
+
return {
|
|
87
|
+
"iterations_completed": self.iterations_completed,
|
|
88
|
+
"commits": self.commits,
|
|
89
|
+
"reverts": self.reverts,
|
|
90
|
+
"stop_reason": self.stop_reason,
|
|
91
|
+
"budget_summary": self.budget_summary,
|
|
92
|
+
"history": [r.to_dict() for r in self.history],
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ------------------------------------------------------------------ #
|
|
97
|
+
# Engine
|
|
98
|
+
# ------------------------------------------------------------------ #
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class EvolutionEngine:
|
|
102
|
+
"""Runs the self-improving development loop."""
|
|
103
|
+
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
project_root: Path,
|
|
107
|
+
provider: LLMProvider,
|
|
108
|
+
budget: BudgetGuard,
|
|
109
|
+
) -> None:
|
|
110
|
+
self._root = project_root.resolve()
|
|
111
|
+
self._provider = provider
|
|
112
|
+
self._budget = budget
|
|
113
|
+
|
|
114
|
+
# Build sub-components
|
|
115
|
+
self._test_runner = TestRunner(project_root=self._root)
|
|
116
|
+
self._commit_mgr = CommitManager(project_root=self._root)
|
|
117
|
+
self._ctx_builder = ContextBuilder(
|
|
118
|
+
project_root=self._root,
|
|
119
|
+
commit_manager=self._commit_mgr,
|
|
120
|
+
)
|
|
121
|
+
self._task_selector = TaskSelector(
|
|
122
|
+
project_root=self._root,
|
|
123
|
+
test_runner=self._test_runner,
|
|
124
|
+
commit_manager=self._commit_mgr,
|
|
125
|
+
)
|
|
126
|
+
self._patch_gen = PatchGenerator(
|
|
127
|
+
project_root=self._root,
|
|
128
|
+
provider=self._provider,
|
|
129
|
+
context_builder=self._ctx_builder,
|
|
130
|
+
budget=self._budget,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# ------------------------------------------------------------------ #
|
|
134
|
+
# Main loop
|
|
135
|
+
# ------------------------------------------------------------------ #
|
|
136
|
+
|
|
137
|
+
def run(self) -> EvolutionResult:
|
|
138
|
+
"""Execute the evolution loop until the budget is exhausted."""
|
|
139
|
+
result = EvolutionResult()
|
|
140
|
+
self._budget.start()
|
|
141
|
+
|
|
142
|
+
# Baseline test run
|
|
143
|
+
last_test = self._test_runner.run()
|
|
144
|
+
logger.info("baseline tests: %s", last_test.summary_line())
|
|
145
|
+
|
|
146
|
+
iteration = 0
|
|
147
|
+
while self._budget.can_continue():
|
|
148
|
+
iteration += 1
|
|
149
|
+
iter_start = time.time()
|
|
150
|
+
record = IterationRecord(iteration=iteration, tests_before=last_test.passed)
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
last_test = self._run_iteration(
|
|
154
|
+
iteration, last_test, record, result,
|
|
155
|
+
)
|
|
156
|
+
except Exception as exc:
|
|
157
|
+
record.error = f"unexpected error: {exc}"
|
|
158
|
+
logger.error("iter %d: %s", iteration, record.error)
|
|
159
|
+
|
|
160
|
+
record.duration_seconds = time.time() - iter_start
|
|
161
|
+
result.history.append(record)
|
|
162
|
+
self._budget.record_iteration()
|
|
163
|
+
|
|
164
|
+
result.iterations_completed = iteration
|
|
165
|
+
result.stop_reason = self._budget.stop_reason() or "completed"
|
|
166
|
+
result.budget_summary = self._budget.summary()
|
|
167
|
+
|
|
168
|
+
# Persist history
|
|
169
|
+
self._write_history(result)
|
|
170
|
+
return result
|
|
171
|
+
|
|
172
|
+
# ------------------------------------------------------------------ #
|
|
173
|
+
# Single iteration
|
|
174
|
+
# ------------------------------------------------------------------ #
|
|
175
|
+
|
|
176
|
+
def _run_iteration(
|
|
177
|
+
self,
|
|
178
|
+
iteration: int,
|
|
179
|
+
last_test: TestResult,
|
|
180
|
+
record: IterationRecord,
|
|
181
|
+
result: EvolutionResult,
|
|
182
|
+
) -> TestResult:
|
|
183
|
+
"""Execute one evolution iteration. Returns the latest TestResult."""
|
|
184
|
+
# 1. Select task
|
|
185
|
+
task = self._task_selector.select(last_test)
|
|
186
|
+
record.task_category = task.category
|
|
187
|
+
record.task_description = task.description
|
|
188
|
+
record.target_files = list(task.target_files)
|
|
189
|
+
logger.info("iter %d: %s — %s", iteration, task.category, task.description)
|
|
190
|
+
|
|
191
|
+
# 2. Generate + apply patch
|
|
192
|
+
patch_result = self._patch_gen.generate_and_apply(task)
|
|
193
|
+
record.patch_lines_changed = patch_result.lines_changed
|
|
194
|
+
|
|
195
|
+
if not patch_result.success:
|
|
196
|
+
record.error = patch_result.error
|
|
197
|
+
logger.warning("iter %d: patch failed — %s", iteration, patch_result.error)
|
|
198
|
+
return last_test
|
|
199
|
+
|
|
200
|
+
# 3. Re-run tests
|
|
201
|
+
new_test = self._test_runner.run()
|
|
202
|
+
record.tests_after = new_test.passed
|
|
203
|
+
logger.info(
|
|
204
|
+
"iter %d: tests %d -> %d",
|
|
205
|
+
iteration, last_test.passed, new_test.passed,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# 4. Decide: commit or revert
|
|
209
|
+
if new_test.return_code == 0 and new_test.passed >= last_test.passed:
|
|
210
|
+
self._commit_mgr.stage_files(patch_result.files_changed)
|
|
211
|
+
sha = self._commit_mgr.commit(
|
|
212
|
+
f"evolve: {task.category} — {task.description[:60]}"
|
|
213
|
+
)
|
|
214
|
+
record.committed = True
|
|
215
|
+
record.commit_sha = sha
|
|
216
|
+
result.commits.append(sha)
|
|
217
|
+
logger.info("iter %d: committed %s", iteration, sha)
|
|
218
|
+
return new_test
|
|
219
|
+
|
|
220
|
+
# Revert
|
|
221
|
+
self._commit_mgr.revert_files(patch_result.files_changed)
|
|
222
|
+
record.reverted = True
|
|
223
|
+
result.reverts += 1
|
|
224
|
+
logger.info("iter %d: reverted — tests regressed", iteration)
|
|
225
|
+
return last_test
|
|
226
|
+
|
|
227
|
+
# ------------------------------------------------------------------ #
|
|
228
|
+
# Persistence
|
|
229
|
+
# ------------------------------------------------------------------ #
|
|
230
|
+
|
|
231
|
+
def _write_history(self, result: EvolutionResult) -> None:
|
|
232
|
+
"""Append the evolution run to ``.codexa/evolution_history.json``."""
|
|
233
|
+
history_dir = self._root / ".codexa"
|
|
234
|
+
history_dir.mkdir(parents=True, exist_ok=True)
|
|
235
|
+
history_file = history_dir / "evolution_history.json"
|
|
236
|
+
|
|
237
|
+
runs: list[dict[str, Any]] = []
|
|
238
|
+
if history_file.exists():
|
|
239
|
+
try:
|
|
240
|
+
runs = json.loads(history_file.read_text(encoding="utf-8"))
|
|
241
|
+
except (json.JSONDecodeError, OSError):
|
|
242
|
+
runs = []
|
|
243
|
+
|
|
244
|
+
runs.append(result.to_dict())
|
|
245
|
+
history_file.write_text(
|
|
246
|
+
json.dumps(runs, indent=2, default=str) + "\n",
|
|
247
|
+
encoding="utf-8",
|
|
248
|
+
)
|
|
249
|
+
logger.info("evolution history written to %s", history_file)
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""Patch generator — asks the LLM for a unified diff and applies it.
|
|
2
|
+
|
|
3
|
+
The generator sends a minimal prompt (system + context) to the configured
|
|
4
|
+
LLM provider and parses the response as a unified diff. It then applies
|
|
5
|
+
the diff to the working tree using ``git apply``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
import subprocess
|
|
12
|
+
import tempfile
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from semantic_code_intelligence.evolution.budget_guard import BudgetGuard
|
|
18
|
+
from semantic_code_intelligence.evolution.context_builder import SYSTEM_PROMPT, ContextBuilder
|
|
19
|
+
from semantic_code_intelligence.evolution.task_selector import EvolutionTask
|
|
20
|
+
from semantic_code_intelligence.llm.provider import LLMMessage, LLMProvider, LLMResponse, MessageRole
|
|
21
|
+
from semantic_code_intelligence.utils.logging import get_logger
|
|
22
|
+
|
|
23
|
+
logger = get_logger("evolution.patch_generator")
|
|
24
|
+
|
|
25
|
+
# Safety limits
|
|
26
|
+
_MAX_FILES_CHANGED = 3
|
|
27
|
+
_MAX_LINES_CHANGED = 200
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class PatchResult:
|
|
32
|
+
"""Result of a patch generation + apply attempt."""
|
|
33
|
+
|
|
34
|
+
success: bool = False
|
|
35
|
+
diff_text: str = ""
|
|
36
|
+
files_changed: list[str] = field(default_factory=list)
|
|
37
|
+
lines_changed: int = 0
|
|
38
|
+
llm_response: LLMResponse | None = None
|
|
39
|
+
error: str = ""
|
|
40
|
+
|
|
41
|
+
def to_dict(self) -> dict[str, Any]:
|
|
42
|
+
"""Serialise the patch result to a plain dictionary."""
|
|
43
|
+
return {
|
|
44
|
+
"success": self.success,
|
|
45
|
+
"files_changed": self.files_changed,
|
|
46
|
+
"lines_changed": self.lines_changed,
|
|
47
|
+
"error": self.error,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class PatchGenerator:
|
|
52
|
+
"""Generates and applies patches via LLM."""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
project_root: Path,
|
|
57
|
+
provider: LLMProvider,
|
|
58
|
+
context_builder: ContextBuilder,
|
|
59
|
+
budget: BudgetGuard,
|
|
60
|
+
) -> None:
|
|
61
|
+
self._root = project_root.resolve()
|
|
62
|
+
self._provider = provider
|
|
63
|
+
self._ctx = context_builder
|
|
64
|
+
self._budget = budget
|
|
65
|
+
|
|
66
|
+
def generate_and_apply(self, task: EvolutionTask) -> PatchResult:
|
|
67
|
+
"""Generate a patch and apply it to the working tree.
|
|
68
|
+
|
|
69
|
+
Steps:
|
|
70
|
+
1. Build minimal context prompt.
|
|
71
|
+
2. Call LLM.
|
|
72
|
+
3. Parse unified diff from response.
|
|
73
|
+
4. Validate safety limits.
|
|
74
|
+
5. Apply via ``git apply``.
|
|
75
|
+
"""
|
|
76
|
+
# 1. Build context
|
|
77
|
+
user_msg = self._ctx.build(task)
|
|
78
|
+
prompt_tokens = self._ctx.estimate_tokens(SYSTEM_PROMPT + user_msg)
|
|
79
|
+
|
|
80
|
+
if prompt_tokens > self._budget.tokens_remaining:
|
|
81
|
+
return PatchResult(
|
|
82
|
+
error=f"Prompt ({prompt_tokens} tokens) exceeds remaining budget "
|
|
83
|
+
f"({self._budget.tokens_remaining} tokens)."
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# 2. Call LLM
|
|
87
|
+
messages = [
|
|
88
|
+
LLMMessage(role=MessageRole.SYSTEM, content=SYSTEM_PROMPT),
|
|
89
|
+
LLMMessage(role=MessageRole.USER, content=user_msg),
|
|
90
|
+
]
|
|
91
|
+
try:
|
|
92
|
+
response = self._provider.chat(messages, temperature=0.2, max_tokens=2048)
|
|
93
|
+
except Exception as exc:
|
|
94
|
+
return PatchResult(error=f"LLM call failed: {exc}")
|
|
95
|
+
|
|
96
|
+
total_tokens = response.usage.get("total_tokens", prompt_tokens + len(response.content) // 4)
|
|
97
|
+
self._budget.record_tokens(total_tokens)
|
|
98
|
+
|
|
99
|
+
# 3. Parse diff
|
|
100
|
+
diff_text = _extract_diff(response.content)
|
|
101
|
+
if not diff_text:
|
|
102
|
+
return PatchResult(
|
|
103
|
+
llm_response=response,
|
|
104
|
+
error="LLM response did not contain a valid unified diff.",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# 4. Validate safety limits
|
|
108
|
+
files = _diff_files(diff_text)
|
|
109
|
+
lines = _diff_line_count(diff_text)
|
|
110
|
+
|
|
111
|
+
if len(files) > _MAX_FILES_CHANGED:
|
|
112
|
+
return PatchResult(
|
|
113
|
+
diff_text=diff_text,
|
|
114
|
+
files_changed=files,
|
|
115
|
+
lines_changed=lines,
|
|
116
|
+
error=f"Patch touches {len(files)} files (max {_MAX_FILES_CHANGED}).",
|
|
117
|
+
)
|
|
118
|
+
if lines > _MAX_LINES_CHANGED:
|
|
119
|
+
return PatchResult(
|
|
120
|
+
diff_text=diff_text,
|
|
121
|
+
files_changed=files,
|
|
122
|
+
lines_changed=lines,
|
|
123
|
+
error=f"Patch changes {lines} lines (max {_MAX_LINES_CHANGED}).",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# 5. Apply
|
|
127
|
+
ok, apply_err = _apply_diff(diff_text, self._root)
|
|
128
|
+
if not ok:
|
|
129
|
+
return PatchResult(
|
|
130
|
+
diff_text=diff_text,
|
|
131
|
+
files_changed=files,
|
|
132
|
+
lines_changed=lines,
|
|
133
|
+
error=f"git apply failed: {apply_err}",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return PatchResult(
|
|
137
|
+
success=True,
|
|
138
|
+
diff_text=diff_text,
|
|
139
|
+
files_changed=files,
|
|
140
|
+
lines_changed=lines,
|
|
141
|
+
llm_response=response,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ------------------------------------------------------------------ #
|
|
146
|
+
# Diff parsing helpers
|
|
147
|
+
# ------------------------------------------------------------------ #
|
|
148
|
+
|
|
149
|
+
def _extract_diff(text: str) -> str:
|
|
150
|
+
"""Extract a unified diff block from LLM output.
|
|
151
|
+
|
|
152
|
+
Looks for ``--- a/`` or a fenced code block containing diff content.
|
|
153
|
+
"""
|
|
154
|
+
# Try fenced code block first
|
|
155
|
+
m = re.search(r"```(?:diff)?\s*\n(.*?)```", text, re.DOTALL)
|
|
156
|
+
if m:
|
|
157
|
+
candidate = m.group(1).strip()
|
|
158
|
+
if "--- " in candidate or "+++ " in candidate:
|
|
159
|
+
return candidate
|
|
160
|
+
|
|
161
|
+
# Try raw diff
|
|
162
|
+
lines = text.splitlines()
|
|
163
|
+
diff_lines: list[str] = []
|
|
164
|
+
in_diff = False
|
|
165
|
+
for line in lines:
|
|
166
|
+
if line.startswith("--- ") or line.startswith("+++ ") or line.startswith("@@ "):
|
|
167
|
+
in_diff = True
|
|
168
|
+
if in_diff:
|
|
169
|
+
diff_lines.append(line)
|
|
170
|
+
# Stop on blank line after diff section
|
|
171
|
+
if not line.strip() and diff_lines and diff_lines[-2].strip():
|
|
172
|
+
continue
|
|
173
|
+
return "\n".join(diff_lines).strip()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _diff_files(diff_text: str) -> list[str]:
|
|
177
|
+
"""Extract file paths changed in a unified diff."""
|
|
178
|
+
files: list[str] = []
|
|
179
|
+
for line in diff_text.splitlines():
|
|
180
|
+
if line.startswith("+++ "):
|
|
181
|
+
path = line[4:].strip()
|
|
182
|
+
# Strip "b/" prefix
|
|
183
|
+
if path.startswith("b/"):
|
|
184
|
+
path = path[2:]
|
|
185
|
+
if path and path != "/dev/null" and path not in files:
|
|
186
|
+
files.append(path)
|
|
187
|
+
return files
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _diff_line_count(diff_text: str) -> int:
|
|
191
|
+
"""Count the number of added + removed lines in a diff."""
|
|
192
|
+
count = 0
|
|
193
|
+
for line in diff_text.splitlines():
|
|
194
|
+
if line.startswith("+") and not line.startswith("+++"):
|
|
195
|
+
count += 1
|
|
196
|
+
elif line.startswith("-") and not line.startswith("---"):
|
|
197
|
+
count += 1
|
|
198
|
+
return count
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _apply_diff(diff_text: str, cwd: Path) -> tuple[bool, str]:
|
|
202
|
+
"""Apply a unified diff via ``git apply``. Returns (success, error)."""
|
|
203
|
+
with tempfile.NamedTemporaryFile(
|
|
204
|
+
mode="w", suffix=".patch", delete=False, encoding="utf-8"
|
|
205
|
+
) as f:
|
|
206
|
+
f.write(diff_text)
|
|
207
|
+
patch_path = f.name
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
proc = subprocess.run(
|
|
211
|
+
["git", "apply", "--check", patch_path],
|
|
212
|
+
capture_output=True,
|
|
213
|
+
text=True,
|
|
214
|
+
cwd=str(cwd),
|
|
215
|
+
)
|
|
216
|
+
if proc.returncode != 0:
|
|
217
|
+
return False, proc.stderr.strip()
|
|
218
|
+
|
|
219
|
+
proc = subprocess.run(
|
|
220
|
+
["git", "apply", patch_path],
|
|
221
|
+
capture_output=True,
|
|
222
|
+
text=True,
|
|
223
|
+
cwd=str(cwd),
|
|
224
|
+
)
|
|
225
|
+
if proc.returncode != 0:
|
|
226
|
+
return False, proc.stderr.strip()
|
|
227
|
+
return True, ""
|
|
228
|
+
finally:
|
|
229
|
+
Path(patch_path).unlink(missing_ok=True)
|