differix 0.1.0a1__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.
- agents/editor.py +455 -0
- agents/researcher.py +405 -0
- agents/reviewer.py +374 -0
- cli/model_cli.py +430 -0
- config.py +557 -0
- differix-0.1.0a1.dist-info/METADATA +123 -0
- differix-0.1.0a1.dist-info/RECORD +62 -0
- differix-0.1.0a1.dist-info/WHEEL +5 -0
- differix-0.1.0a1.dist-info/entry_points.txt +2 -0
- differix-0.1.0a1.dist-info/top_level.txt +6 -0
- main.py +2796 -0
- memory/__init__.py +59 -0
- memory/embeddings/__init__.py +29 -0
- memory/embeddings/factory.py +178 -0
- memory/embeddings/providers.py +259 -0
- memory/manager.py +186 -0
- memory/migrator.py +253 -0
- memory/ranker.py +203 -0
- memory/schemas.py +127 -0
- memory/storage.py +552 -0
- tools/agents_chat.py +152 -0
- tools/asset_extractor.py +545 -0
- tools/asset_manager.py +115 -0
- tools/atomic_changeset.py +264 -0
- tools/backup_manager.py +122 -0
- tools/changelog_engine.py +261 -0
- tools/coherence_checker.py +771 -0
- tools/compiler.py +531 -0
- tools/component_normalizer.py +130 -0
- tools/context_assembler.py +631 -0
- tools/contract_validator.py +478 -0
- tools/cost_predictor.py +141 -0
- tools/drift_guard.py +196 -0
- tools/file_manager.py +119 -0
- tools/handshake.py +351 -0
- tools/lesson_manager.py +570 -0
- tools/lesson_memory_bridge.py +398 -0
- tools/llm_client.py +103 -0
- tools/llm_selector.py +362 -0
- tools/model_ranker.py +535 -0
- tools/palette_extractor.py +1043 -0
- tools/plugin_manager.py +204 -0
- tools/project_context_gatherer.py +404 -0
- tools/project_scanner.py +41 -0
- tools/project_structure.py +244 -0
- tools/reasoning_logger.py +78 -0
- tools/reflection.py +452 -0
- tools/sandbox_utils.py +56 -0
- tools/security/__init__.py +0 -0
- tools/security/behavior_guard.py +83 -0
- tools/security/verification_firewall.py +113 -0
- tools/security_filter.py +213 -0
- tools/session_adr.py +311 -0
- tools/skill_manager.py +129 -0
- tools/state_manager.py +383 -0
- tools/structure_index.py +570 -0
- tools/test_schema_consistency.py +19 -0
- tools/verification_firewall.py +113 -0
- tools/visual_capture.py +65 -0
- tools/visual_diff.py +220 -0
- tools/visual_reviewer.py +79 -0
- tools/web_search.py +371 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/changelog_engine.py
|
|
3
|
+
--------------------------
|
|
4
|
+
Project-scoped iteration log manager for VegeX.
|
|
5
|
+
|
|
6
|
+
Logs live at `<project_root>/iteration_logs/`, not at the process cwd.
|
|
7
|
+
This removes the class of bug where a `vegex` process launched from one
|
|
8
|
+
directory writes logs into a different project's tree.
|
|
9
|
+
|
|
10
|
+
Public API:
|
|
11
|
+
- ChangelogEngine(project_root=".", session_id=None)
|
|
12
|
+
- engine.generate_diff(old_text, new_text, filename) -> dict
|
|
13
|
+
- engine.record_iteration(state, diff_data="") -> None
|
|
14
|
+
|
|
15
|
+
Disk layout (per project):
|
|
16
|
+
<project_root>/iteration_logs/session_YYYY-MM-DD_HH-MM-SS.json
|
|
17
|
+
<project_root>/iteration_logs/session_YYYY-MM-DD_HH-MM-SS_SUMMARY.md
|
|
18
|
+
<project_root>/iteration_logs/archived_logs.zip (rotated, capped)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import difflib
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import tempfile
|
|
25
|
+
import zipfile
|
|
26
|
+
from datetime import datetime
|
|
27
|
+
|
|
28
|
+
ARCHIVE_NAME = "archived_logs.zip"
|
|
29
|
+
LOGS_SUBDIR = "iteration_logs"
|
|
30
|
+
DEFAULT_MAX_RECENT = 10
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ChangelogEngine:
|
|
34
|
+
def __init__(self, project_root: str = ".", session_id: str = None):
|
|
35
|
+
self.project_root = os.path.abspath(project_root)
|
|
36
|
+
self.logs_dir = os.path.join(self.project_root, LOGS_SUBDIR)
|
|
37
|
+
|
|
38
|
+
os.makedirs(self.logs_dir, exist_ok=True)
|
|
39
|
+
|
|
40
|
+
if not session_id:
|
|
41
|
+
session_id = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
|
42
|
+
self.session_id = session_id
|
|
43
|
+
self.log_filepath = os.path.join(
|
|
44
|
+
self.logs_dir, f"session_{session_id}.json"
|
|
45
|
+
)
|
|
46
|
+
self.md_filepath = os.path.join(
|
|
47
|
+
self.logs_dir, f"session_{session_id}_SUMMARY.md"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# 1. Rotate/archive old logs first (never touches the current session)
|
|
51
|
+
self._clean_and_archive_logs()
|
|
52
|
+
|
|
53
|
+
# 2. Create the current session log file
|
|
54
|
+
self._init_log_file()
|
|
55
|
+
|
|
56
|
+
# ------------------------------------------------------------------ #
|
|
57
|
+
# Log rotation
|
|
58
|
+
# ------------------------------------------------------------------ #
|
|
59
|
+
def _clean_and_archive_logs(self, max_recent: int = DEFAULT_MAX_RECENT) -> None:
|
|
60
|
+
"""
|
|
61
|
+
- Delete empty log files (no iterations recorded).
|
|
62
|
+
- Keep the most recent `max_recent` non-empty logs.
|
|
63
|
+
- Zip and remove older non-empty logs (and their summary MDs).
|
|
64
|
+
"""
|
|
65
|
+
if not os.path.isdir(self.logs_dir):
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
valid_logs = []
|
|
69
|
+
archive_path = os.path.join(self.logs_dir, ARCHIVE_NAME)
|
|
70
|
+
current_log_abs = os.path.abspath(self.log_filepath)
|
|
71
|
+
|
|
72
|
+
for fname in sorted(os.listdir(self.logs_dir)):
|
|
73
|
+
if not fname.endswith(".json") or fname == ARCHIVE_NAME:
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
fpath = os.path.join(self.logs_dir, fname)
|
|
77
|
+
|
|
78
|
+
if os.path.abspath(fpath) == current_log_abs:
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
is_empty = True
|
|
82
|
+
try:
|
|
83
|
+
with open(fpath, "r", encoding="utf-8") as f:
|
|
84
|
+
data = json.load(f)
|
|
85
|
+
if isinstance(data, dict) and data.get("iterations"):
|
|
86
|
+
is_empty = False
|
|
87
|
+
elif isinstance(data, list) and len(data) > 0:
|
|
88
|
+
is_empty = False
|
|
89
|
+
except Exception:
|
|
90
|
+
is_empty = True
|
|
91
|
+
|
|
92
|
+
md_path = fpath.replace(".json", "_SUMMARY.md")
|
|
93
|
+
|
|
94
|
+
if is_empty:
|
|
95
|
+
for p in (fpath, md_path):
|
|
96
|
+
try:
|
|
97
|
+
os.remove(p)
|
|
98
|
+
except OSError:
|
|
99
|
+
pass
|
|
100
|
+
else:
|
|
101
|
+
try:
|
|
102
|
+
mtime = os.path.getmtime(fpath)
|
|
103
|
+
except OSError:
|
|
104
|
+
continue
|
|
105
|
+
valid_logs.append((fpath, mtime))
|
|
106
|
+
|
|
107
|
+
# Newest first
|
|
108
|
+
valid_logs.sort(key=lambda x: x[1], reverse=True)
|
|
109
|
+
old_logs = valid_logs[max_recent:]
|
|
110
|
+
|
|
111
|
+
if not old_logs:
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
with zipfile.ZipFile(
|
|
116
|
+
archive_path, "a", compression=zipfile.ZIP_DEFLATED
|
|
117
|
+
) as zipf:
|
|
118
|
+
for fpath, _ in old_logs:
|
|
119
|
+
if os.path.exists(fpath):
|
|
120
|
+
zipf.write(fpath, arcname=os.path.basename(fpath))
|
|
121
|
+
try:
|
|
122
|
+
os.remove(fpath)
|
|
123
|
+
except OSError:
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
md_path = fpath.replace(".json", "_SUMMARY.md")
|
|
127
|
+
if os.path.exists(md_path):
|
|
128
|
+
zipf.write(md_path, arcname=os.path.basename(md_path))
|
|
129
|
+
try:
|
|
130
|
+
os.remove(md_path)
|
|
131
|
+
except OSError:
|
|
132
|
+
pass
|
|
133
|
+
except Exception as exc:
|
|
134
|
+
print(f"[ChangelogEngine] Archive rotation failed: {exc}")
|
|
135
|
+
|
|
136
|
+
# ------------------------------------------------------------------ #
|
|
137
|
+
# Session init
|
|
138
|
+
# ------------------------------------------------------------------ #
|
|
139
|
+
def _init_log_file(self) -> None:
|
|
140
|
+
if os.path.exists(self.log_filepath):
|
|
141
|
+
return
|
|
142
|
+
initial_data = {
|
|
143
|
+
"session_id": self.session_id,
|
|
144
|
+
"project_root": self.project_root,
|
|
145
|
+
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
146
|
+
"iterations": [],
|
|
147
|
+
}
|
|
148
|
+
_atomic_write_json(self.log_filepath, initial_data)
|
|
149
|
+
|
|
150
|
+
# ------------------------------------------------------------------ #
|
|
151
|
+
# Diff helper
|
|
152
|
+
# ------------------------------------------------------------------ #
|
|
153
|
+
def generate_diff(self, old_text: str, new_text: str, filename: str) -> dict:
|
|
154
|
+
old_lines = old_text.splitlines(keepends=True)
|
|
155
|
+
new_lines = new_text.splitlines(keepends=True)
|
|
156
|
+
|
|
157
|
+
diff = list(
|
|
158
|
+
difflib.unified_diff(
|
|
159
|
+
old_lines, new_lines, fromfile=filename, tofile=filename
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
added = sum(
|
|
164
|
+
1 for line in diff
|
|
165
|
+
if line.startswith("+") and not line.startswith("+++")
|
|
166
|
+
)
|
|
167
|
+
removed = sum(
|
|
168
|
+
1 for line in diff
|
|
169
|
+
if line.startswith("-") and not line.startswith("---")
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
clean_diff = "".join(diff[:40])
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
"file": filename,
|
|
176
|
+
"lines_added": added,
|
|
177
|
+
"lines_removed": removed,
|
|
178
|
+
"unified_diff": (
|
|
179
|
+
clean_diff if clean_diff
|
|
180
|
+
else "File created from zero/no diffs"
|
|
181
|
+
),
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# ------------------------------------------------------------------ #
|
|
185
|
+
# Iteration recording
|
|
186
|
+
# ------------------------------------------------------------------ #
|
|
187
|
+
def record_iteration(self, state: dict, diff_data="") -> None:
|
|
188
|
+
os.makedirs(self.logs_dir, exist_ok=True)
|
|
189
|
+
|
|
190
|
+
# Load existing
|
|
191
|
+
data = {}
|
|
192
|
+
if os.path.exists(self.log_filepath):
|
|
193
|
+
try:
|
|
194
|
+
with open(self.log_filepath, "r", encoding="utf-8") as f:
|
|
195
|
+
loaded = json.load(f)
|
|
196
|
+
if isinstance(loaded, dict):
|
|
197
|
+
data = loaded
|
|
198
|
+
elif isinstance(loaded, list):
|
|
199
|
+
data = {"iterations": loaded}
|
|
200
|
+
except (json.JSONDecodeError, FileNotFoundError, OSError):
|
|
201
|
+
data = {}
|
|
202
|
+
|
|
203
|
+
if not isinstance(data.get("iterations"), list):
|
|
204
|
+
data["iterations"] = []
|
|
205
|
+
|
|
206
|
+
# Defensive: generated_code can be a dict OR a str (editor full-rewrite mode)
|
|
207
|
+
gen = state.get("generated_code") if isinstance(state, dict) else None
|
|
208
|
+
if isinstance(gen, dict):
|
|
209
|
+
target_file = gen.get("target_file")
|
|
210
|
+
else:
|
|
211
|
+
target_file = None
|
|
212
|
+
if not target_file:
|
|
213
|
+
target_file = (
|
|
214
|
+
(state.get("active_target_file") if isinstance(state, dict) else None)
|
|
215
|
+
or (state.get("target_file") if isinstance(state, dict) else None)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
if isinstance(gen, dict):
|
|
219
|
+
generated_snapshot = {
|
|
220
|
+
"target_file": gen.get("target_file"),
|
|
221
|
+
"apply_result": gen.get("apply_result"),
|
|
222
|
+
}
|
|
223
|
+
elif isinstance(gen, str):
|
|
224
|
+
generated_snapshot = {"kind": "str", "length": len(gen)}
|
|
225
|
+
else:
|
|
226
|
+
generated_snapshot = None
|
|
227
|
+
|
|
228
|
+
new_entry = {
|
|
229
|
+
"iteration": (state.get("iteration_count", 0) if isinstance(state, dict) else 0),
|
|
230
|
+
"step": (state.get("current_step") if isinstance(state, dict) else None),
|
|
231
|
+
"target_file": target_file,
|
|
232
|
+
"generated_code_summary": generated_snapshot,
|
|
233
|
+
"review": (state.get("review_result") if isinstance(state, dict) else None),
|
|
234
|
+
"diff": diff_data,
|
|
235
|
+
}
|
|
236
|
+
data["iterations"].append(new_entry)
|
|
237
|
+
|
|
238
|
+
_atomic_write_json(self.log_filepath, data)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ---------------------------------------------------------------------------
|
|
242
|
+
# Atomic write helper
|
|
243
|
+
# ---------------------------------------------------------------------------
|
|
244
|
+
def _atomic_write_json(path: str, data) -> None:
|
|
245
|
+
"""Temp + os.replace. Readers never see a partial file."""
|
|
246
|
+
target_dir = os.path.dirname(os.path.abspath(path))
|
|
247
|
+
os.makedirs(target_dir, exist_ok=True)
|
|
248
|
+
fd, tmp = tempfile.mkstemp(dir=target_dir, suffix=".json.tmp")
|
|
249
|
+
try:
|
|
250
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
251
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
252
|
+
os.replace(tmp, path)
|
|
253
|
+
except Exception:
|
|
254
|
+
try:
|
|
255
|
+
os.unlink(tmp)
|
|
256
|
+
except OSError:
|
|
257
|
+
pass
|
|
258
|
+
raise
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
__all__ = ["ChangelogEngine"]
|