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.
Files changed (62) hide show
  1. agents/editor.py +455 -0
  2. agents/researcher.py +405 -0
  3. agents/reviewer.py +374 -0
  4. cli/model_cli.py +430 -0
  5. config.py +557 -0
  6. differix-0.1.0a1.dist-info/METADATA +123 -0
  7. differix-0.1.0a1.dist-info/RECORD +62 -0
  8. differix-0.1.0a1.dist-info/WHEEL +5 -0
  9. differix-0.1.0a1.dist-info/entry_points.txt +2 -0
  10. differix-0.1.0a1.dist-info/top_level.txt +6 -0
  11. main.py +2796 -0
  12. memory/__init__.py +59 -0
  13. memory/embeddings/__init__.py +29 -0
  14. memory/embeddings/factory.py +178 -0
  15. memory/embeddings/providers.py +259 -0
  16. memory/manager.py +186 -0
  17. memory/migrator.py +253 -0
  18. memory/ranker.py +203 -0
  19. memory/schemas.py +127 -0
  20. memory/storage.py +552 -0
  21. tools/agents_chat.py +152 -0
  22. tools/asset_extractor.py +545 -0
  23. tools/asset_manager.py +115 -0
  24. tools/atomic_changeset.py +264 -0
  25. tools/backup_manager.py +122 -0
  26. tools/changelog_engine.py +261 -0
  27. tools/coherence_checker.py +771 -0
  28. tools/compiler.py +531 -0
  29. tools/component_normalizer.py +130 -0
  30. tools/context_assembler.py +631 -0
  31. tools/contract_validator.py +478 -0
  32. tools/cost_predictor.py +141 -0
  33. tools/drift_guard.py +196 -0
  34. tools/file_manager.py +119 -0
  35. tools/handshake.py +351 -0
  36. tools/lesson_manager.py +570 -0
  37. tools/lesson_memory_bridge.py +398 -0
  38. tools/llm_client.py +103 -0
  39. tools/llm_selector.py +362 -0
  40. tools/model_ranker.py +535 -0
  41. tools/palette_extractor.py +1043 -0
  42. tools/plugin_manager.py +204 -0
  43. tools/project_context_gatherer.py +404 -0
  44. tools/project_scanner.py +41 -0
  45. tools/project_structure.py +244 -0
  46. tools/reasoning_logger.py +78 -0
  47. tools/reflection.py +452 -0
  48. tools/sandbox_utils.py +56 -0
  49. tools/security/__init__.py +0 -0
  50. tools/security/behavior_guard.py +83 -0
  51. tools/security/verification_firewall.py +113 -0
  52. tools/security_filter.py +213 -0
  53. tools/session_adr.py +311 -0
  54. tools/skill_manager.py +129 -0
  55. tools/state_manager.py +383 -0
  56. tools/structure_index.py +570 -0
  57. tools/test_schema_consistency.py +19 -0
  58. tools/verification_firewall.py +113 -0
  59. tools/visual_capture.py +65 -0
  60. tools/visual_diff.py +220 -0
  61. tools/visual_reviewer.py +79 -0
  62. 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"]