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,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)
|