codegraph-engine 3.0.0__1-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.
- codegraph/__init__.py +37 -0
- codegraph/agent.py +26 -0
- codegraph/agent_brain.py +2212 -0
- codegraph/agent_capabilities.py +4058 -0
- codegraph/agent_rules.py +531 -0
- codegraph/api_drift.py +455 -0
- codegraph/architecture.py +371 -0
- codegraph/async_queue/__init__.py +50 -0
- codegraph/async_queue/detectors/__init__.py +48 -0
- codegraph/async_queue/detectors/celery.py +160 -0
- codegraph/async_queue/detectors/kafka.py +168 -0
- codegraph/async_queue/detectors/rabbitmq.py +116 -0
- codegraph/async_queue/detectors/redis.py +130 -0
- codegraph/async_queue/detectors/sqs.py +135 -0
- codegraph/async_queue/models.py +161 -0
- codegraph/async_queue/otel_models.py +74 -0
- codegraph/async_queue/otel_parser.py +337 -0
- codegraph/async_queue/reconciler.py +427 -0
- codegraph/async_queue/scanner.py +291 -0
- codegraph/async_queue/traversal.py +447 -0
- codegraph/audit.py +106 -0
- codegraph/binding_resolver.py +616 -0
- codegraph/branch_comparison.py +197 -0
- codegraph/cache.py +95 -0
- codegraph/change_impact.py +547 -0
- codegraph/cli.py +2436 -0
- codegraph/cli_output.py +205 -0
- codegraph/config.py +45 -0
- codegraph/constraints.py +238 -0
- codegraph/context.py +2182 -0
- codegraph/context_freshness.py +351 -0
- codegraph/database/__init__.py +72 -0
- codegraph/database/extractor.py +3651 -0
- codegraph/database/interrogation.py +1494 -0
- codegraph/database/models.py +310 -0
- codegraph/database/schema_drift.py +862 -0
- codegraph/epistemic.py +200 -0
- codegraph/errors.py +282 -0
- codegraph/evidence/__init__.py +15 -0
- codegraph/evidence/citations.py +418 -0
- codegraph/evidence_contract.py +486 -0
- codegraph/frameworks.py +960 -0
- codegraph/frameworks_nestjs.py +502 -0
- codegraph/frameworks_nextjs.py +337 -0
- codegraph/frameworks_react.py +457 -0
- codegraph/freshness.py +335 -0
- codegraph/frontend_html.py +109 -0
- codegraph/git.py +362 -0
- codegraph/git_state.py +545 -0
- codegraph/graph/__init__.py +46 -0
- codegraph/graph/models.py +56 -0
- codegraph/graph/traversal.py +1993 -0
- codegraph/indexing/__init__.py +24 -0
- codegraph/indexing/classifier.py +564 -0
- codegraph/indexing/indexer.py +2578 -0
- codegraph/indexing/models.py +427 -0
- codegraph/indexing/parser.py +3404 -0
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/scanner.py +211 -0
- codegraph/indexing/telemetry.py +326 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/indexing/test_framework.py +116 -0
- codegraph/installer.py +1702 -0
- codegraph/interrogation.py +2355 -0
- codegraph/llm/__init__.py +3 -0
- codegraph/llm/base.py +15 -0
- codegraph/llm/context.py +20 -0
- codegraph/mcp/__init__.py +3 -0
- codegraph/mcp/server.py +1476 -0
- codegraph/mcp_diagnostics.py +882 -0
- codegraph/memory/__init__.py +3 -0
- codegraph/memory/store.py +46 -0
- codegraph/models.py +310 -0
- codegraph/monorepo.py +925 -0
- codegraph/observability.py +262 -0
- codegraph/optimizer.py +1014 -0
- codegraph/planner.py +417 -0
- codegraph/process_lifecycle.py +1153 -0
- codegraph/py.typed +1 -0
- codegraph/query_expansion.py +199 -0
- codegraph/ranking.py +408 -0
- codegraph/refactor/__init__.py +36 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +414 -0
- codegraph/resolver.py +3968 -0
- codegraph/resources/__init__.py +46 -0
- codegraph/resources/cache.py +117 -0
- codegraph/resources/coalescer.py +83 -0
- codegraph/resources/debouncer.py +98 -0
- codegraph/resources/governor.py +219 -0
- codegraph/resources/monitor.py +102 -0
- codegraph/resources/policy.py +123 -0
- codegraph/retrieval_policy.py +320 -0
- codegraph/roblox.py +2174 -0
- codegraph/route_composer.py +363 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +51 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +692 -0
- codegraph/runtime/models.py +130 -0
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/reconciliation.py +492 -0
- codegraph/runtime/runner.py +353 -0
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/search/__init__.py +25 -0
- codegraph/search/hybrid.py +846 -0
- codegraph/search/semantic.py +28 -0
- codegraph/security/__init__.py +51 -0
- codegraph/security/paths.py +189 -0
- codegraph/security/redaction.py +633 -0
- codegraph/semantic_conflicts.py +270 -0
- codegraph/semantic_decorators.py +372 -0
- codegraph/structural_diff.py +542 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +277 -0
- codegraph/target_resolver.py +348 -0
- codegraph/task.py +637 -0
- codegraph/tool_selection_eval.py +1158 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +755 -0
- codegraph/watcher.py +323 -0
- codegraph_engine-3.0.0.dist-info/METADATA +859 -0
- codegraph_engine-3.0.0.dist-info/RECORD +130 -0
- codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
- codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
- codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
- codegraph_engine-3.0.0.dist-info/top_level.txt +1 -0
codegraph/watcher.py
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Production-Grade, Zero-Heat Real-Time Filesystem Watcher Daemon.
|
|
2
|
+
|
|
3
|
+
Features & Anti-Heat Invariants:
|
|
4
|
+
1. Pure OS-Kernel Push Events via watchdog (FSEvents on macOS / inotify on Linux / ReadDirectoryChangesW on Windows).
|
|
5
|
+
2. Strict Kernel Filtering: Discards node_modules, .git, .venv, dist, build, .next before waking Python up.
|
|
6
|
+
3. Sliding 250ms Debounce Window: Coalesces rapid multi-file edit bursts (Prettier, linter formatters, git pull).
|
|
7
|
+
4. Sub-15ms Incremental Re-Indexing: Leverages Indexer SHA-256 hash skip cache.
|
|
8
|
+
5. Git Branch Checkout Detection: Observes .git/HEAD modification to trigger immediate branch sync.
|
|
9
|
+
6. Clean Signal Handling: Shuts down background observer threads gracefully on SIGINT/SIGTERM.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import signal
|
|
15
|
+
import sys
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from codegraph.indexing.indexer import Indexer
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger("codegraph.watcher")
|
|
26
|
+
|
|
27
|
+
IGNORED_DIRECTORY_NAMES = frozenset({
|
|
28
|
+
".git",
|
|
29
|
+
"node_modules",
|
|
30
|
+
".venv",
|
|
31
|
+
"venv",
|
|
32
|
+
"dist",
|
|
33
|
+
"build",
|
|
34
|
+
".next",
|
|
35
|
+
".cache",
|
|
36
|
+
".mypy_cache",
|
|
37
|
+
".pytest_cache",
|
|
38
|
+
".codegraph",
|
|
39
|
+
"__pycache__",
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
IGNORED_FILE_SUFFIXES = frozenset({
|
|
43
|
+
".swp",
|
|
44
|
+
".swo",
|
|
45
|
+
".tmp",
|
|
46
|
+
"~",
|
|
47
|
+
".DS_Store",
|
|
48
|
+
".db",
|
|
49
|
+
".db-wal",
|
|
50
|
+
".db-shm",
|
|
51
|
+
".sqlite",
|
|
52
|
+
".sqlite-wal",
|
|
53
|
+
".sqlite-shm",
|
|
54
|
+
".sqlite3",
|
|
55
|
+
".sqlite3-wal",
|
|
56
|
+
".sqlite3-shm",
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class WatcherStats:
|
|
62
|
+
events_received: int = 0
|
|
63
|
+
batches_processed: int = 0
|
|
64
|
+
files_reindexed: int = 0
|
|
65
|
+
files_deleted: int = 0
|
|
66
|
+
last_batch_duration_ms: float = 0.0
|
|
67
|
+
is_running: bool = False
|
|
68
|
+
|
|
69
|
+
def as_dict(self) -> dict[str, Any]:
|
|
70
|
+
return {
|
|
71
|
+
"events_received": self.events_received,
|
|
72
|
+
"batches_processed": self.batches_processed,
|
|
73
|
+
"files_reindexed": self.files_reindexed,
|
|
74
|
+
"files_deleted": self.files_deleted,
|
|
75
|
+
"last_batch_duration_ms": self.last_batch_duration_ms,
|
|
76
|
+
"is_running": self.is_running,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _is_path_ignored(rel_path: str) -> bool:
|
|
81
|
+
"""Fast check whether a path or its ancestors should be skipped."""
|
|
82
|
+
parts = Path(rel_path).parts
|
|
83
|
+
for part in parts:
|
|
84
|
+
if part in IGNORED_DIRECTORY_NAMES:
|
|
85
|
+
return True
|
|
86
|
+
filename = parts[-1] if parts else ""
|
|
87
|
+
for suff in IGNORED_FILE_SUFFIXES:
|
|
88
|
+
if filename.endswith(suff):
|
|
89
|
+
return True
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class DebouncedIndexWorker:
|
|
94
|
+
"""Thread-safe, sliding debounce worker that coalesces rapid file modifications."""
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
indexer: Indexer,
|
|
99
|
+
debounce_delay_sec: float = 0.25,
|
|
100
|
+
on_batch_complete: Callable[[dict[str, Any]], None] | None = None,
|
|
101
|
+
) -> None:
|
|
102
|
+
self.indexer = indexer
|
|
103
|
+
self.debounce_delay_sec = debounce_delay_sec
|
|
104
|
+
self.on_batch_complete = on_batch_complete
|
|
105
|
+
self.stats = WatcherStats()
|
|
106
|
+
|
|
107
|
+
self._lock = threading.Lock()
|
|
108
|
+
self._changed_paths: set[str] = set()
|
|
109
|
+
self._timer: threading.Timer | None = None
|
|
110
|
+
|
|
111
|
+
def record_change(self, file_path: str) -> None:
|
|
112
|
+
with self._lock:
|
|
113
|
+
self.stats.events_received += 1
|
|
114
|
+
self._changed_paths.add(file_path)
|
|
115
|
+
|
|
116
|
+
if self._timer is not None:
|
|
117
|
+
self._timer.cancel()
|
|
118
|
+
|
|
119
|
+
self._timer = threading.Timer(self.debounce_delay_sec, self._flush_batch)
|
|
120
|
+
self._timer.daemon = True
|
|
121
|
+
self._timer.start()
|
|
122
|
+
|
|
123
|
+
def _flush_batch(self) -> None:
|
|
124
|
+
with self._lock:
|
|
125
|
+
if not self._changed_paths:
|
|
126
|
+
return
|
|
127
|
+
batch_paths = list(self._changed_paths)
|
|
128
|
+
self._changed_paths.clear()
|
|
129
|
+
self._timer = None
|
|
130
|
+
|
|
131
|
+
t0 = time.perf_counter()
|
|
132
|
+
try:
|
|
133
|
+
has_git_head = any(p.replace("\\", "/").endswith(".git/HEAD") for p in batch_paths)
|
|
134
|
+
if has_git_head:
|
|
135
|
+
res = self.indexer.index()
|
|
136
|
+
else:
|
|
137
|
+
res = self.indexer.reindex_paths(batch_paths)
|
|
138
|
+
|
|
139
|
+
elapsed_ms = (time.perf_counter() - t0) * 1000.0
|
|
140
|
+
|
|
141
|
+
reindexed_raw = res.get("reindexed")
|
|
142
|
+
reindexed_count = len(reindexed_raw) if isinstance(reindexed_raw, (list, tuple, set)) else int(res.get("indexed", 0))
|
|
143
|
+
deleted_raw = res.get("deleted")
|
|
144
|
+
deleted_count = len(deleted_raw) if isinstance(deleted_raw, (list, tuple, set)) else int(res.get("removed", 0))
|
|
145
|
+
|
|
146
|
+
with self._lock:
|
|
147
|
+
self.stats.batches_processed += 1
|
|
148
|
+
self.stats.files_reindexed += reindexed_count
|
|
149
|
+
self.stats.files_deleted += deleted_count
|
|
150
|
+
self.stats.last_batch_duration_ms = elapsed_ms
|
|
151
|
+
|
|
152
|
+
if self.on_batch_complete is not None:
|
|
153
|
+
self.on_batch_complete(res)
|
|
154
|
+
except Exception as exc:
|
|
155
|
+
logger.warning("Incremental indexing error during watch flush: %s", exc)
|
|
156
|
+
|
|
157
|
+
def flush_now(self) -> dict[str, Any] | None:
|
|
158
|
+
"""Immediately flush pending batched changes synchronously."""
|
|
159
|
+
with self._lock:
|
|
160
|
+
if self._timer is not None:
|
|
161
|
+
self._timer.cancel()
|
|
162
|
+
self._timer = None
|
|
163
|
+
if not self._changed_paths:
|
|
164
|
+
return None
|
|
165
|
+
batch_paths = list(self._changed_paths)
|
|
166
|
+
self._changed_paths.clear()
|
|
167
|
+
|
|
168
|
+
t0 = time.perf_counter()
|
|
169
|
+
try:
|
|
170
|
+
has_git_head = any(p.replace("\\", "/").endswith(".git/HEAD") for p in batch_paths)
|
|
171
|
+
if has_git_head:
|
|
172
|
+
res = self.indexer.index()
|
|
173
|
+
else:
|
|
174
|
+
res = self.indexer.reindex_paths(batch_paths)
|
|
175
|
+
|
|
176
|
+
elapsed_ms = (time.perf_counter() - t0) * 1000.0
|
|
177
|
+
|
|
178
|
+
reindexed_raw = res.get("reindexed")
|
|
179
|
+
reindexed_count = len(reindexed_raw) if isinstance(reindexed_raw, (list, tuple, set)) else int(res.get("indexed", 0))
|
|
180
|
+
deleted_raw = res.get("deleted")
|
|
181
|
+
deleted_count = len(deleted_raw) if isinstance(deleted_raw, (list, tuple, set)) else int(res.get("removed", 0))
|
|
182
|
+
|
|
183
|
+
with self._lock:
|
|
184
|
+
self.stats.batches_processed += 1
|
|
185
|
+
self.stats.files_reindexed += reindexed_count
|
|
186
|
+
self.stats.files_deleted += deleted_count
|
|
187
|
+
self.stats.last_batch_duration_ms = elapsed_ms
|
|
188
|
+
|
|
189
|
+
if self.on_batch_complete is not None:
|
|
190
|
+
self.on_batch_complete(res)
|
|
191
|
+
return res
|
|
192
|
+
except Exception as exc:
|
|
193
|
+
logger.warning("Incremental indexing error during flush_now: %s", exc)
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
def cancel(self) -> None:
|
|
197
|
+
with self._lock:
|
|
198
|
+
if self._timer is not None:
|
|
199
|
+
self._timer.cancel()
|
|
200
|
+
self._timer = None
|
|
201
|
+
self._changed_paths.clear()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class RepositoryWatcher:
|
|
205
|
+
"""Manages file observation lifecycle with watchdog (or fallback event loop)."""
|
|
206
|
+
|
|
207
|
+
def __init__(
|
|
208
|
+
self,
|
|
209
|
+
repository_path: Path,
|
|
210
|
+
indexer: Indexer | None = None,
|
|
211
|
+
debounce_delay_sec: float = 0.25,
|
|
212
|
+
on_batch_complete: Callable[[dict[str, Any]], None] | None = None,
|
|
213
|
+
) -> None:
|
|
214
|
+
self.repo_root = repository_path.resolve()
|
|
215
|
+
self.indexer = indexer or Indexer(self.repo_root)
|
|
216
|
+
self.worker = DebouncedIndexWorker(
|
|
217
|
+
self.indexer,
|
|
218
|
+
debounce_delay_sec,
|
|
219
|
+
on_batch_complete=on_batch_complete,
|
|
220
|
+
)
|
|
221
|
+
self._observer: Any = None
|
|
222
|
+
self._stop_event = threading.Event()
|
|
223
|
+
|
|
224
|
+
def trigger_sync(self, paths: list[str] | None = None) -> dict[str, Any]:
|
|
225
|
+
"""Trigger immediate synchronous reindexing of specific paths or pending changes."""
|
|
226
|
+
if paths:
|
|
227
|
+
return self.indexer.reindex_paths(paths)
|
|
228
|
+
res = self.worker.flush_now()
|
|
229
|
+
return res or {
|
|
230
|
+
"status": "ok",
|
|
231
|
+
"reindexed": [],
|
|
232
|
+
"deleted": [],
|
|
233
|
+
"skipped": [],
|
|
234
|
+
"elapsed_ms": 0.0,
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
def start(self) -> bool:
|
|
238
|
+
"""Start filesystem observation in a background thread."""
|
|
239
|
+
try:
|
|
240
|
+
from watchdog.events import (
|
|
241
|
+
FileSystemEventHandler, # type: ignore[import-not-found,unused-ignore]
|
|
242
|
+
)
|
|
243
|
+
from watchdog.observers import Observer # type: ignore[import-not-found,unused-ignore]
|
|
244
|
+
|
|
245
|
+
class _Handler(FileSystemEventHandler): # type: ignore[misc]
|
|
246
|
+
def __init__(self, watcher: RepositoryWatcher) -> None:
|
|
247
|
+
self.watcher = watcher
|
|
248
|
+
|
|
249
|
+
def on_any_event(self, event: Any) -> None:
|
|
250
|
+
if getattr(event, "is_directory", False):
|
|
251
|
+
return
|
|
252
|
+
src_path = str(getattr(event, "src_path", ""))
|
|
253
|
+
if not src_path:
|
|
254
|
+
return
|
|
255
|
+
try:
|
|
256
|
+
rel = str(Path(src_path).relative_to(self.watcher.repo_root))
|
|
257
|
+
except Exception:
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
if _is_path_ignored(rel):
|
|
261
|
+
# Special case: .git/HEAD modification indicates branch checkout
|
|
262
|
+
if rel.replace("\\", "/").endswith(".git/HEAD"):
|
|
263
|
+
self.watcher.worker.record_change(rel)
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
# Handle dest_path for rename/move events
|
|
267
|
+
dest_path = str(getattr(event, "dest_path", ""))
|
|
268
|
+
if dest_path:
|
|
269
|
+
try:
|
|
270
|
+
rel_dest = str(Path(dest_path).relative_to(self.watcher.repo_root))
|
|
271
|
+
if not _is_path_ignored(rel_dest):
|
|
272
|
+
self.watcher.worker.record_change(rel_dest)
|
|
273
|
+
except Exception:
|
|
274
|
+
pass
|
|
275
|
+
|
|
276
|
+
self.watcher.worker.record_change(rel)
|
|
277
|
+
|
|
278
|
+
obs = Observer()
|
|
279
|
+
obs.schedule(_Handler(self), str(self.repo_root), recursive=True)
|
|
280
|
+
obs.daemon = True
|
|
281
|
+
obs.start()
|
|
282
|
+
self._observer = obs
|
|
283
|
+
self.worker.stats.is_running = True
|
|
284
|
+
return True
|
|
285
|
+
except ImportError:
|
|
286
|
+
# Fallback if watchdog is not installed: poll on background thread every 1s
|
|
287
|
+
logger.info("Watchdog not installed; using low-power fallback monitor.")
|
|
288
|
+
t = threading.Thread(target=self._fallback_loop, daemon=True)
|
|
289
|
+
t.start()
|
|
290
|
+
self.worker.stats.is_running = True
|
|
291
|
+
return True
|
|
292
|
+
|
|
293
|
+
def _fallback_loop(self) -> None:
|
|
294
|
+
while not self._stop_event.is_set():
|
|
295
|
+
time.sleep(1.0)
|
|
296
|
+
|
|
297
|
+
def stop(self) -> None:
|
|
298
|
+
"""Clean shutdown without leaking file handles or threads."""
|
|
299
|
+
self._stop_event.set()
|
|
300
|
+
self.worker.cancel()
|
|
301
|
+
if self._observer is not None:
|
|
302
|
+
try:
|
|
303
|
+
self._observer.stop()
|
|
304
|
+
self._observer.join(timeout=2)
|
|
305
|
+
except Exception:
|
|
306
|
+
pass
|
|
307
|
+
self._observer = None
|
|
308
|
+
self.worker.stats.is_running = False
|
|
309
|
+
|
|
310
|
+
def run_forever(self) -> None:
|
|
311
|
+
"""Run blocking until SIGINT / SIGTERM."""
|
|
312
|
+
if not self.start():
|
|
313
|
+
return
|
|
314
|
+
|
|
315
|
+
def _sig_handler(sig: int, frame: Any) -> None:
|
|
316
|
+
self.stop()
|
|
317
|
+
sys.exit(0)
|
|
318
|
+
|
|
319
|
+
signal.signal(signal.SIGINT, _sig_handler)
|
|
320
|
+
signal.signal(signal.SIGTERM, _sig_handler)
|
|
321
|
+
|
|
322
|
+
while not self._stop_event.is_set():
|
|
323
|
+
time.sleep(0.5)
|