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.
Files changed (130) hide show
  1. codegraph/__init__.py +37 -0
  2. codegraph/agent.py +26 -0
  3. codegraph/agent_brain.py +2212 -0
  4. codegraph/agent_capabilities.py +4058 -0
  5. codegraph/agent_rules.py +531 -0
  6. codegraph/api_drift.py +455 -0
  7. codegraph/architecture.py +371 -0
  8. codegraph/async_queue/__init__.py +50 -0
  9. codegraph/async_queue/detectors/__init__.py +48 -0
  10. codegraph/async_queue/detectors/celery.py +160 -0
  11. codegraph/async_queue/detectors/kafka.py +168 -0
  12. codegraph/async_queue/detectors/rabbitmq.py +116 -0
  13. codegraph/async_queue/detectors/redis.py +130 -0
  14. codegraph/async_queue/detectors/sqs.py +135 -0
  15. codegraph/async_queue/models.py +161 -0
  16. codegraph/async_queue/otel_models.py +74 -0
  17. codegraph/async_queue/otel_parser.py +337 -0
  18. codegraph/async_queue/reconciler.py +427 -0
  19. codegraph/async_queue/scanner.py +291 -0
  20. codegraph/async_queue/traversal.py +447 -0
  21. codegraph/audit.py +106 -0
  22. codegraph/binding_resolver.py +616 -0
  23. codegraph/branch_comparison.py +197 -0
  24. codegraph/cache.py +95 -0
  25. codegraph/change_impact.py +547 -0
  26. codegraph/cli.py +2436 -0
  27. codegraph/cli_output.py +205 -0
  28. codegraph/config.py +45 -0
  29. codegraph/constraints.py +238 -0
  30. codegraph/context.py +2182 -0
  31. codegraph/context_freshness.py +351 -0
  32. codegraph/database/__init__.py +72 -0
  33. codegraph/database/extractor.py +3651 -0
  34. codegraph/database/interrogation.py +1494 -0
  35. codegraph/database/models.py +310 -0
  36. codegraph/database/schema_drift.py +862 -0
  37. codegraph/epistemic.py +200 -0
  38. codegraph/errors.py +282 -0
  39. codegraph/evidence/__init__.py +15 -0
  40. codegraph/evidence/citations.py +418 -0
  41. codegraph/evidence_contract.py +486 -0
  42. codegraph/frameworks.py +960 -0
  43. codegraph/frameworks_nestjs.py +502 -0
  44. codegraph/frameworks_nextjs.py +337 -0
  45. codegraph/frameworks_react.py +457 -0
  46. codegraph/freshness.py +335 -0
  47. codegraph/frontend_html.py +109 -0
  48. codegraph/git.py +362 -0
  49. codegraph/git_state.py +545 -0
  50. codegraph/graph/__init__.py +46 -0
  51. codegraph/graph/models.py +56 -0
  52. codegraph/graph/traversal.py +1993 -0
  53. codegraph/indexing/__init__.py +24 -0
  54. codegraph/indexing/classifier.py +564 -0
  55. codegraph/indexing/indexer.py +2578 -0
  56. codegraph/indexing/models.py +427 -0
  57. codegraph/indexing/parser.py +3404 -0
  58. codegraph/indexing/pool.py +136 -0
  59. codegraph/indexing/scanner.py +211 -0
  60. codegraph/indexing/telemetry.py +326 -0
  61. codegraph/indexing/templates.py +418 -0
  62. codegraph/indexing/test_framework.py +116 -0
  63. codegraph/installer.py +1702 -0
  64. codegraph/interrogation.py +2355 -0
  65. codegraph/llm/__init__.py +3 -0
  66. codegraph/llm/base.py +15 -0
  67. codegraph/llm/context.py +20 -0
  68. codegraph/mcp/__init__.py +3 -0
  69. codegraph/mcp/server.py +1476 -0
  70. codegraph/mcp_diagnostics.py +882 -0
  71. codegraph/memory/__init__.py +3 -0
  72. codegraph/memory/store.py +46 -0
  73. codegraph/models.py +310 -0
  74. codegraph/monorepo.py +925 -0
  75. codegraph/observability.py +262 -0
  76. codegraph/optimizer.py +1014 -0
  77. codegraph/planner.py +417 -0
  78. codegraph/process_lifecycle.py +1153 -0
  79. codegraph/py.typed +1 -0
  80. codegraph/query_expansion.py +199 -0
  81. codegraph/ranking.py +408 -0
  82. codegraph/refactor/__init__.py +36 -0
  83. codegraph/refactor/models.py +114 -0
  84. codegraph/refactor/renamer.py +844 -0
  85. codegraph/refactor/transactions.py +414 -0
  86. codegraph/resolver.py +3968 -0
  87. codegraph/resources/__init__.py +46 -0
  88. codegraph/resources/cache.py +117 -0
  89. codegraph/resources/coalescer.py +83 -0
  90. codegraph/resources/debouncer.py +98 -0
  91. codegraph/resources/governor.py +219 -0
  92. codegraph/resources/monitor.py +102 -0
  93. codegraph/resources/policy.py +123 -0
  94. codegraph/retrieval_policy.py +320 -0
  95. codegraph/roblox.py +2174 -0
  96. codegraph/route_composer.py +363 -0
  97. codegraph/route_topology.py +172 -0
  98. codegraph/runtime/__init__.py +51 -0
  99. codegraph/runtime/collector.py +155 -0
  100. codegraph/runtime/db_watcher.py +191 -0
  101. codegraph/runtime/ingestor.py +692 -0
  102. codegraph/runtime/models.py +130 -0
  103. codegraph/runtime/port_inspector.py +304 -0
  104. codegraph/runtime/reconciliation.py +492 -0
  105. codegraph/runtime/runner.py +353 -0
  106. codegraph/runtime/trace_linker.py +175 -0
  107. codegraph/search/__init__.py +25 -0
  108. codegraph/search/hybrid.py +846 -0
  109. codegraph/search/semantic.py +28 -0
  110. codegraph/security/__init__.py +51 -0
  111. codegraph/security/paths.py +189 -0
  112. codegraph/security/redaction.py +633 -0
  113. codegraph/semantic_conflicts.py +270 -0
  114. codegraph/semantic_decorators.py +372 -0
  115. codegraph/structural_diff.py +542 -0
  116. codegraph/styles.py +144 -0
  117. codegraph/symbol_history.py +277 -0
  118. codegraph/target_resolver.py +348 -0
  119. codegraph/task.py +637 -0
  120. codegraph/tool_selection_eval.py +1158 -0
  121. codegraph/tsconfig.py +236 -0
  122. codegraph/ui/__init__.py +6 -0
  123. codegraph/ui/server.py +755 -0
  124. codegraph/watcher.py +323 -0
  125. codegraph_engine-3.0.0.dist-info/METADATA +859 -0
  126. codegraph_engine-3.0.0.dist-info/RECORD +130 -0
  127. codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
  128. codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
  129. codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
  130. 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)