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,244 @@
1
+ """
2
+ tools/project_structure.py
3
+ --------------------------
4
+ Human-readable project tree generator for VegeX.
5
+
6
+ Produces `PROJECT_STRUCTURE.md` at the project root and a small metadata
7
+ sidecar at `.vegex/tree_meta.json` (gitignored).
8
+
9
+ Change detection uses a SHA-256 content hash over every tracked file — NOT
10
+ the mtime+size pair used by the older implementation, which silently
11
+ missed same-second edits and same-size replacements.
12
+
13
+ Public API:
14
+ - generate_or_update_structure(project_path) -> dict
15
+ {
16
+ "updated": bool,
17
+ "fingerprint": str, # sha256 over (relpath + content-sha256)
18
+ "file_count": int,
19
+ "structure_text": str,
20
+ }
21
+ - get_structure_text(project_path) -> str
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import hashlib
27
+ import json
28
+ import os
29
+ import tempfile
30
+ from datetime import datetime
31
+ from pathlib import Path
32
+ from typing import Any, Dict, List
33
+
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Configuration
37
+ # ---------------------------------------------------------------------------
38
+ _VEGEX_DIR = ".vegex"
39
+ _TREE_META_FILE = "tree_meta.json"
40
+ _TREE_MD_FILE = "PROJECT_STRUCTURE.md"
41
+
42
+ _IGNORE_DIRS = {
43
+ ".git", ".vegex", "__pycache__", ".pytest_cache", ".mypy_cache",
44
+ ".ruff_cache", ".tox", ".venv", "venv", "env", "node_modules",
45
+ "bin", "obj", "build", "dist", ".idea", ".vscode",
46
+ "iteration_logs", "AI_Orchestrator_Backups",
47
+ }
48
+
49
+ _IGNORE_FILES = {
50
+ ".env",
51
+ "PROJECT_STRUCTURE.md",
52
+ "structure_meta.json", # deprecated sidecar from prior versions
53
+ "projects_registry.json",
54
+ "skills_config.json",
55
+ "active_models.json",
56
+ "models_cache.json",
57
+ ".agent_bindings.json",
58
+ ".openrouter_models_cache.json",
59
+ }
60
+
61
+ _IGNORE_SUFFIXES = (
62
+ ".pyc", ".pyo", ".pyd", ".so", ".dll", ".dylib",
63
+ ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".webp",
64
+ ".zip", ".tar", ".gz", ".7z", ".rar",
65
+ ".mp4", ".webm", ".mov", ".mp3", ".wav",
66
+ )
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # File walk
71
+ # ---------------------------------------------------------------------------
72
+ def _iter_tracked_files(root: Path) -> List[Path]:
73
+ """Deterministic walk: sorted dirs, sorted files, filters applied."""
74
+ out: List[Path] = []
75
+ for dirpath, dirnames, filenames in os.walk(root):
76
+ dirnames[:] = sorted(
77
+ d for d in dirnames
78
+ if d not in _IGNORE_DIRS and not d.startswith(".")
79
+ )
80
+ for fname in sorted(filenames):
81
+ if fname in _IGNORE_FILES:
82
+ continue
83
+ if any(fname.endswith(s) for s in _IGNORE_SUFFIXES):
84
+ continue
85
+ out.append(Path(dirpath) / fname)
86
+ return out
87
+
88
+
89
+ # ---------------------------------------------------------------------------
90
+ # Fingerprint
91
+ # ---------------------------------------------------------------------------
92
+ def _compute_fingerprint(root: Path, files: List[Path]) -> str:
93
+ """
94
+ Deterministic fingerprint:
95
+ sha256 over sorted [relpath \\0 sha256(content) \\n] entries.
96
+ """
97
+ hasher = hashlib.sha256()
98
+ for f in files:
99
+ try:
100
+ rel = f.relative_to(root).as_posix()
101
+ except ValueError:
102
+ continue
103
+ hasher.update(rel.encode("utf-8"))
104
+ hasher.update(b"\0")
105
+ try:
106
+ content = f.read_bytes()
107
+ hasher.update(hashlib.sha256(content).digest())
108
+ except OSError:
109
+ hasher.update(b"<unreadable>")
110
+ hasher.update(b"\n")
111
+ return hasher.hexdigest()
112
+
113
+
114
+ # ---------------------------------------------------------------------------
115
+ # Tree rendering
116
+ # ---------------------------------------------------------------------------
117
+ def _build_tree_text(root: Path, files: List[Path]) -> str:
118
+ tree: Dict[str, Any] = {}
119
+ for f in files:
120
+ try:
121
+ rel = f.relative_to(root).as_posix()
122
+ except ValueError:
123
+ continue
124
+ parts = rel.split("/")
125
+ current = tree
126
+ for part in parts:
127
+ current = current.setdefault(part, {})
128
+
129
+ def render(node: Dict[str, Any], prefix: str = "") -> List[str]:
130
+ lines: List[str] = []
131
+ keys = sorted(node.keys())
132
+ for i, key in enumerate(keys):
133
+ is_last = (i == len(keys) - 1)
134
+ connector = "└── " if is_last else "├── "
135
+ if node[key]:
136
+ lines.append(f"{prefix}{connector}{key}/")
137
+ new_prefix = prefix + (" " if is_last else "│ ")
138
+ lines.extend(render(node[key], new_prefix))
139
+ else:
140
+ lines.append(f"{prefix}{connector}{key}")
141
+ return lines
142
+
143
+ body = "\n".join(render(tree))
144
+ generated = datetime.utcnow().isoformat(timespec="seconds") + "Z"
145
+ return (
146
+ f"# Project Structure\n\n"
147
+ f"Generated on: {generated}\n\n"
148
+ f"```\n{body}\n```\n"
149
+ )
150
+
151
+
152
+ # ---------------------------------------------------------------------------
153
+ # Atomic I/O
154
+ # ---------------------------------------------------------------------------
155
+ def _atomic_write(path: Path, content: str) -> None:
156
+ """Write via temp file + os.replace so readers never see a partial file."""
157
+ path.parent.mkdir(parents=True, exist_ok=True)
158
+ fd, tmp = tempfile.mkstemp(dir=str(path.parent), suffix=".tmp")
159
+ try:
160
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
161
+ f.write(content)
162
+ os.replace(tmp, path)
163
+ except Exception:
164
+ try:
165
+ os.unlink(tmp)
166
+ except OSError:
167
+ pass
168
+ raise
169
+
170
+
171
+ def _load_meta(meta_path: Path) -> Dict[str, Any]:
172
+ if not meta_path.exists():
173
+ return {}
174
+ try:
175
+ return json.loads(meta_path.read_text(encoding="utf-8"))
176
+ except Exception:
177
+ return {}
178
+
179
+
180
+ # ---------------------------------------------------------------------------
181
+ # Public API
182
+ # ---------------------------------------------------------------------------
183
+ def generate_or_update_structure(project_path: str) -> Dict[str, Any]:
184
+ """
185
+ Walk the project, compute a content-hash fingerprint, and (re)write the
186
+ human-readable tree if anything changed.
187
+
188
+ Idempotent: two calls without source changes → second call reports
189
+ updated=False and returns the identical structure_text.
190
+ """
191
+ root = Path(project_path).resolve()
192
+ files = _iter_tracked_files(root)
193
+ current_fp = _compute_fingerprint(root, files)
194
+
195
+ veg_dir = root / _VEGEX_DIR
196
+ meta_path = veg_dir / _TREE_META_FILE
197
+ md_path = root / _TREE_MD_FILE
198
+
199
+ meta = _load_meta(meta_path)
200
+ old_fp = meta.get("fingerprint")
201
+
202
+ structure_text = ""
203
+ updated = False
204
+
205
+ if current_fp != old_fp or not md_path.exists():
206
+ structure_text = _build_tree_text(root, files)
207
+ _atomic_write(md_path, structure_text)
208
+ _atomic_write(
209
+ meta_path,
210
+ json.dumps(
211
+ {
212
+ "schema_version": "2.0.0",
213
+ "fingerprint": current_fp,
214
+ "updated_at": datetime.utcnow().isoformat(timespec="seconds") + "Z",
215
+ "file_count": len(files),
216
+ },
217
+ indent=2,
218
+ sort_keys=True,
219
+ ),
220
+ )
221
+ updated = True
222
+ else:
223
+ try:
224
+ structure_text = md_path.read_text(encoding="utf-8")
225
+ except OSError:
226
+ structure_text = ""
227
+
228
+ return {
229
+ "updated": updated,
230
+ "fingerprint": current_fp,
231
+ "file_count": len(files),
232
+ "structure_text": structure_text,
233
+ }
234
+
235
+
236
+ def get_structure_text(project_path: str) -> str:
237
+ """Convenience accessor — returns just the rendered MD text."""
238
+ return generate_or_update_structure(project_path)["structure_text"]
239
+
240
+
241
+ __all__ = [
242
+ "generate_or_update_structure",
243
+ "get_structure_text",
244
+ ]
@@ -0,0 +1,78 @@
1
+ import os
2
+ import json
3
+ import ast
4
+ from datetime import datetime
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+ from rich.markdown import Markdown
8
+
9
+ console = Console()
10
+
11
+ AGENT_STYLES = {
12
+ "researcher": {"color": "cyan", "title": "RESEARCHER PLANNING"},
13
+ "editor": {"color": "yellow", "title": "EDITOR CODEGEN"},
14
+ "reviewer": {"color": "magenta", "title": "REVIEWER CRITIC"},
15
+ "system": {"color": "green", "title": "SYSTEM ORCHESTRATOR"}
16
+ }
17
+
18
+ def _sanitize_value(val):
19
+ """Parses stringified Python/JSON lists or dicts back into native types."""
20
+ if isinstance(val, str):
21
+ val_trimmed = val.strip()
22
+ if (val_trimmed.startswith("[") and val_trimmed.endswith("]")) or \
23
+ (val_trimmed.startswith("{") and val_trimmed.endswith("}")):
24
+ try:
25
+ return json.loads(val_trimmed)
26
+ except Exception:
27
+ try:
28
+ return ast.literal_eval(val_trimmed)
29
+ except Exception:
30
+ pass
31
+ return val
32
+
33
+ def log_reasoning(agent_role: str, step: str, reasoning_data: dict, project_root: str = "."):
34
+ status_dir = os.path.join(project_root, "status")
35
+ os.makedirs(status_dir, exist_ok=True)
36
+
37
+ jsonl_path = os.path.join(status_dir, "reasoning_log.jsonl")
38
+ md_path = os.path.join(status_dir, "last_thoughts.md")
39
+ timestamp = datetime.now().isoformat()
40
+
41
+ # Clean empty values & sanitize stringified blobs
42
+ clean_data = {}
43
+ for k, v in reasoning_data.items():
44
+ if v not in (None, "", [], {}):
45
+ clean_data[k] = _sanitize_value(v)
46
+
47
+ role_key = agent_role.lower()
48
+ style_info = AGENT_STYLES.get(role_key, {"color": "white", "title": agent_role.upper()})
49
+
50
+ formatted_body = ""
51
+ for key, val in clean_data.items():
52
+ field_name = key.replace("_", " ").title()
53
+ if isinstance(val, (dict, list)):
54
+ val_str = json.dumps(val, ensure_ascii=False, indent=2)
55
+ formatted_body += f"**{field_name}:**\n```json\n{val_str}\n```\n\n"
56
+ else:
57
+ formatted_body += f"**{field_name}:**\n{val}\n\n"
58
+
59
+ panel = Panel(
60
+ Markdown(formatted_body.strip() if formatted_body else "*No detailed payload provided.*"),
61
+ title=f"[{style_info['color']} bold]{style_info['title']} | Step: {step}[/]",
62
+ subtitle=f"[dim]{timestamp}[/dim]",
63
+ border_style=style_info["color"],
64
+ padding=(1, 2)
65
+ )
66
+ console.print(panel)
67
+
68
+ record = {
69
+ "timestamp": timestamp,
70
+ "agent": agent_role,
71
+ "step": step,
72
+ "data": clean_data
73
+ }
74
+ with open(jsonl_path, "a", encoding="utf-8") as f:
75
+ f.write(json.dumps(record, ensure_ascii=False) + "\n")
76
+
77
+ with open(md_path, "w", encoding="utf-8") as f:
78
+ f.write(f"# Agent Thought Log\n\n- **Agent:** `{agent_role.upper()}`\n- **Step:** `{step}`\n\n" + formatted_body)