codegraph-engine 2.2.1__py3-none-any.whl → 3.0.0__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 (82) hide show
  1. codegraph/__init__.py +1 -1
  2. codegraph/agent_brain.py +172 -2
  3. codegraph/agent_capabilities.py +564 -6
  4. codegraph/agent_rules.py +1 -1
  5. codegraph/api_drift.py +455 -0
  6. codegraph/async_queue/__init__.py +50 -0
  7. codegraph/async_queue/detectors/__init__.py +48 -0
  8. codegraph/async_queue/detectors/celery.py +160 -0
  9. codegraph/async_queue/detectors/kafka.py +168 -0
  10. codegraph/async_queue/detectors/rabbitmq.py +116 -0
  11. codegraph/async_queue/detectors/redis.py +130 -0
  12. codegraph/async_queue/detectors/sqs.py +135 -0
  13. codegraph/async_queue/models.py +161 -0
  14. codegraph/async_queue/otel_models.py +74 -0
  15. codegraph/async_queue/otel_parser.py +337 -0
  16. codegraph/async_queue/reconciler.py +427 -0
  17. codegraph/async_queue/scanner.py +291 -0
  18. codegraph/async_queue/traversal.py +447 -0
  19. codegraph/branch_comparison.py +197 -0
  20. codegraph/change_impact.py +547 -0
  21. codegraph/cli.py +992 -3
  22. codegraph/cli_output.py +17 -2
  23. codegraph/context.py +38 -11
  24. codegraph/context_freshness.py +351 -0
  25. codegraph/database/__init__.py +14 -0
  26. codegraph/database/extractor.py +712 -17
  27. codegraph/database/interrogation.py +388 -2
  28. codegraph/database/schema_drift.py +862 -0
  29. codegraph/epistemic.py +7 -0
  30. codegraph/evidence_contract.py +21 -0
  31. codegraph/frameworks.py +171 -6
  32. codegraph/frameworks_nestjs.py +502 -0
  33. codegraph/frameworks_nextjs.py +337 -0
  34. codegraph/frameworks_react.py +457 -0
  35. codegraph/freshness.py +2 -0
  36. codegraph/frontend_html.py +109 -0
  37. codegraph/git.py +2 -0
  38. codegraph/git_state.py +545 -0
  39. codegraph/indexing/__init__.py +7 -1
  40. codegraph/indexing/indexer.py +291 -38
  41. codegraph/indexing/models.py +6 -0
  42. codegraph/indexing/parser.py +239 -0
  43. codegraph/indexing/pool.py +136 -0
  44. codegraph/indexing/scanner.py +6 -0
  45. codegraph/indexing/templates.py +418 -0
  46. codegraph/interrogation.py +128 -20
  47. codegraph/mcp/server.py +258 -27
  48. codegraph/models.py +7 -0
  49. codegraph/monorepo.py +3 -0
  50. codegraph/optimizer.py +42 -5
  51. codegraph/process_lifecycle.py +31 -13
  52. codegraph/refactor/__init__.py +36 -0
  53. codegraph/refactor/models.py +114 -0
  54. codegraph/refactor/renamer.py +844 -0
  55. codegraph/refactor/transactions.py +403 -0
  56. codegraph/resolver.py +1202 -159
  57. codegraph/retrieval_policy.py +6 -0
  58. codegraph/route_topology.py +172 -0
  59. codegraph/runtime/__init__.py +19 -0
  60. codegraph/runtime/collector.py +155 -0
  61. codegraph/runtime/db_watcher.py +191 -0
  62. codegraph/runtime/ingestor.py +30 -10
  63. codegraph/runtime/port_inspector.py +304 -0
  64. codegraph/runtime/runner.py +155 -6
  65. codegraph/runtime/trace_linker.py +175 -0
  66. codegraph/search/hybrid.py +26 -0
  67. codegraph/semantic_conflicts.py +270 -0
  68. codegraph/semantic_decorators.py +34 -3
  69. codegraph/structural_diff.py +542 -0
  70. codegraph/styles.py +144 -0
  71. codegraph/symbol_history.py +277 -0
  72. codegraph/tsconfig.py +236 -0
  73. codegraph/ui/__init__.py +6 -0
  74. codegraph/ui/server.py +715 -0
  75. codegraph/watcher.py +323 -0
  76. {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/METADATA +257 -39
  77. codegraph_engine-3.0.0.dist-info/RECORD +129 -0
  78. codegraph_engine-2.2.1.dist-info/RECORD +0 -87
  79. {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/WHEEL +0 -0
  80. {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/entry_points.txt +0 -0
  81. {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/licenses/LICENSE +0 -0
  82. {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/top_level.txt +0 -0
@@ -14,8 +14,9 @@ from __future__ import annotations
14
14
  import hashlib
15
15
  import json
16
16
  import sqlite3
17
+ import threading
17
18
  import time
18
- from collections.abc import Callable, Iterator
19
+ from collections.abc import Callable, Iterable, Iterator
19
20
  from contextlib import contextmanager
20
21
  from pathlib import Path
21
22
  from typing import Any
@@ -32,6 +33,7 @@ from codegraph.resources import (
32
33
  ResourceGovernor,
33
34
  TaskPriority,
34
35
  get_global_governor,
36
+ get_graph_cache,
35
37
  get_parse_cache,
36
38
  )
37
39
 
@@ -44,7 +46,8 @@ from .models import (
44
46
  Symbol,
45
47
  )
46
48
  from .parser import PARSER_VERSION, parse
47
- from .scanner import scan
49
+ from .pool import SQLiteConnectionPool
50
+ from .scanner import LANGUAGES, scan
48
51
  from .telemetry import IndexingTelemetry, get_process_rss_mb
49
52
 
50
53
  SCHEMA_VERSION = 8
@@ -488,14 +491,33 @@ class Indexer:
488
491
  self.max_wildcard_expansions = max_wildcard_expansions
489
492
  self.last_epistemic_findings: list[dict[str, object]] = []
490
493
  self.last_telemetry: IndexingTelemetry | None = None
494
+ self._pool: SQLiteConnectionPool | None = None
495
+ self._schema_lock = threading.Lock()
496
+ self._schema_verified = False
497
+
498
+ @property
499
+ def pool(self) -> SQLiteConnectionPool:
500
+ if self._pool is None:
501
+ self._pool = SQLiteConnectionPool(
502
+ self.db_path,
503
+ max_size=16,
504
+ timeout=30.0,
505
+ initializer=self._ensure_schema,
506
+ )
507
+ return self._pool
491
508
 
492
509
  def connect(self) -> sqlite3.Connection:
493
- con = sqlite3.connect(self.db_path)
510
+ """Create a dedicated unpooled connection (for indexing or long-lived writes)."""
511
+ con = sqlite3.connect(
512
+ self.db_path,
513
+ check_same_thread=False,
514
+ timeout=30.0,
515
+ )
494
516
  con.row_factory = make_compat_row
495
517
  con.execute("PRAGMA journal_mode = WAL")
496
518
  con.execute("PRAGMA synchronous = NORMAL")
497
519
  con.execute("PRAGMA foreign_keys = ON")
498
- con.execute("PRAGMA busy_timeout = 15000")
520
+ con.execute("PRAGMA busy_timeout = 30000")
499
521
  con.execute("PRAGMA cache_size = -64000")
500
522
  con.execute("PRAGMA temp_store = MEMORY")
501
523
  self._ensure_schema(con)
@@ -503,51 +525,77 @@ class Indexer:
503
525
 
504
526
  @contextmanager
505
527
  def session(self) -> Iterator[sqlite3.Connection]:
506
- """Commit on success, roll back on failure, always close handle."""
507
- con = self.connect()
528
+ """Borrow a pooled connection; commit on success, rollback on failure, return to pool."""
529
+ con, is_owner = self.pool.acquire()
508
530
  try:
509
531
  yield con
510
- con.commit()
532
+ self.pool.release(con, is_owner=is_owner, rollback=False)
511
533
  except BaseException:
512
- con.rollback()
534
+ self.pool.release(con, is_owner=is_owner, rollback=True)
513
535
  raise
514
- finally:
515
- con.close()
536
+
537
+ def reset_pool(self) -> None:
538
+ """Close pooled connections and clear schema verification flag."""
539
+ if self._pool is not None:
540
+ self._pool.close_all()
541
+ self._pool = None
542
+ self._schema_verified = False
543
+
544
+ def close(self) -> None:
545
+ """Dispose of resources and close pooled connections."""
546
+ self.reset_pool()
547
+
548
+ def __del__(self) -> None:
549
+ try:
550
+ self.close()
551
+ except Exception:
552
+ pass
516
553
 
517
554
  def _ensure_schema(self, con: sqlite3.Connection) -> None:
518
555
  """Migrate schema via PRAGMA user_version non-destructively."""
519
- current_version = con.execute("PRAGMA user_version").fetchone()[0]
556
+ if self._schema_verified:
557
+ return
558
+ with self._schema_lock:
559
+ if self._schema_verified:
560
+ return
561
+ current_version = con.execute("PRAGMA user_version").fetchone()[0]
562
+
563
+ if current_version == 0:
564
+ try:
565
+ row = con.execute("SELECT version FROM schema_version").fetchone()
566
+ if row:
567
+ current_version = int(row[0])
568
+ except sqlite3.OperationalError:
569
+ pass
520
570
 
521
- if current_version == 0:
522
- try:
523
- row = con.execute("SELECT version FROM schema_version").fetchone()
524
- if row:
525
- current_version = int(row[0])
526
- except sqlite3.OperationalError:
527
- pass
571
+ target_version = SCHEMA_VERSION
528
572
 
529
- target_version = SCHEMA_VERSION
573
+ if current_version < target_version:
574
+ if current_version > 0:
575
+ self._migrate_schema(con, current_version, target_version)
576
+ else:
577
+ con.executescript(SCHEMA)
530
578
 
531
- if current_version < target_version:
532
- if current_version > 0:
533
- self._migrate_schema(con, current_version, target_version)
579
+ con.execute(f"PRAGMA user_version = {target_version}")
580
+ try:
581
+ con.execute("DROP TABLE IF EXISTS schema_version")
582
+ except sqlite3.OperationalError:
583
+ pass
584
+ con.commit()
534
585
  else:
535
- con.executescript(SCHEMA)
586
+ existing_core = con.execute(
587
+ "SELECT count(*) FROM sqlite_master WHERE type='table' "
588
+ "AND name IN ('files', 'symbols', 'graph_edges', 'db_tables', 'runtime_traces')"
589
+ ).fetchone()[0]
590
+ if existing_core < 5:
591
+ con.executescript(SCHEMA)
592
+ con.commit()
593
+
594
+ from codegraph.async_queue import ensure_async_tables
595
+
596
+ ensure_async_tables(con)
597
+ self._schema_verified = True
536
598
 
537
- con.execute(f"PRAGMA user_version = {target_version}")
538
- try:
539
- con.execute("DROP TABLE IF EXISTS schema_version")
540
- except sqlite3.OperationalError:
541
- pass
542
- con.commit()
543
- else:
544
- existing_core = con.execute(
545
- "SELECT count(*) FROM sqlite_master WHERE type='table' "
546
- "AND name IN ('files', 'symbols', 'graph_edges', 'db_tables', 'runtime_traces')"
547
- ).fetchone()[0]
548
- if existing_core < 5:
549
- con.executescript(SCHEMA)
550
- con.commit()
551
599
 
552
600
  def _migrate_schema(
553
601
  self, con: sqlite3.Connection, from_version: int, to_version: int
@@ -860,6 +908,9 @@ class Indexer:
860
908
  telemetry=telemetry,
861
909
  progress_callback=progress_callback,
862
910
  )
911
+ from codegraph.async_queue import AsyncQueueScanner
912
+
913
+ AsyncQueueScanner(self.repository).scan_repository(con)
863
914
  p_post.files_processed = indexed + len(stale)
864
915
  p_post.items_processed = indexed + len(stale)
865
916
  # Invalidate context cache on index update
@@ -968,8 +1019,206 @@ class Indexer:
968
1019
  }
969
1020
  if parse_failed_count > 0:
970
1021
  result["parse_failed"] = parse_failed_count
1022
+ self.reset_pool()
971
1023
  return result
972
1024
 
1025
+ def reindex_paths(
1026
+ self,
1027
+ paths: Iterable[str | Path],
1028
+ ) -> dict[str, Any]:
1029
+ """Incrementally reindex a set of changed or deleted file paths in sub-15ms.
1030
+
1031
+ Bypasses full repository filesystem scanning and only touches the specified files,
1032
+ updating their AST facts, re-running affected reference resolution, busting in-memory
1033
+ caches, and advancing index_generation atomically.
1034
+ """
1035
+ t0 = time.perf_counter()
1036
+ from codegraph.freshness import current_commit, save_commit
1037
+ from codegraph.watcher import _is_path_ignored
1038
+
1039
+ # 1. Normalize and deduplicate paths relative to self.repository
1040
+ normalized_paths: set[str] = set()
1041
+ for p in paths:
1042
+ try:
1043
+ p_path = Path(p)
1044
+ if p_path.is_absolute():
1045
+ rel = str(p_path.resolve().relative_to(self.repository)).replace("\\", "/")
1046
+ else:
1047
+ rel = str(p_path).replace("\\", "/").lstrip("./")
1048
+ except Exception:
1049
+ continue
1050
+
1051
+ if not rel or _is_path_ignored(rel):
1052
+ continue
1053
+ normalized_paths.add(rel)
1054
+
1055
+ if not normalized_paths:
1056
+ return {
1057
+ "status": "ok",
1058
+ "scanned": 0,
1059
+ "indexed": 0,
1060
+ "removed": 0,
1061
+ "reindexed": [],
1062
+ "deleted": [],
1063
+ "skipped": [],
1064
+ "parse_failed": [],
1065
+ "generation": 0,
1066
+ "elapsed_ms": round((time.perf_counter() - t0) * 1000.0, 2),
1067
+ }
1068
+
1069
+ # 2. Inspect filesystem vs SQLite to determine what to delete, update, or skip
1070
+ con = self.connect()
1071
+ try:
1072
+ to_delete: list[str] = []
1073
+ to_update: list[tuple[str, str, str, str, bool]] = []
1074
+ skipped: list[str] = []
1075
+
1076
+ for rel_path in sorted(normalized_paths):
1077
+ full_path = self.repository / rel_path
1078
+ existing_row = con.execute("SELECT hash, status FROM files WHERE path=?", (rel_path,)).fetchone()
1079
+
1080
+ if not full_path.is_file():
1081
+ # File was deleted from disk
1082
+ if existing_row is not None:
1083
+ to_delete.append(rel_path)
1084
+ else:
1085
+ skipped.append(rel_path)
1086
+ continue
1087
+
1088
+ # File exists on disk
1089
+ suffix = full_path.suffix.lower()
1090
+ lang = LANGUAGES.get(suffix)
1091
+ if not lang:
1092
+ skipped.append(rel_path)
1093
+ continue
1094
+
1095
+ try:
1096
+ content = full_path.read_text(encoding="utf-8", errors="replace")
1097
+ except Exception:
1098
+ skipped.append(rel_path)
1099
+ continue
1100
+
1101
+ digest = hashlib.sha256(content.encode("utf-8")).hexdigest()
1102
+
1103
+ if existing_row is not None and existing_row[0] == digest and existing_row[1] == "ok":
1104
+ # Identical digest, no AST or file modification
1105
+ skipped.append(rel_path)
1106
+ else:
1107
+ to_update.append((rel_path, lang, content, digest, existing_row is not None))
1108
+
1109
+ # If nothing changed, return early
1110
+ grow = con.execute("SELECT value FROM metadata WHERE key='index_generation'").fetchone()
1111
+ current_gen = int(grow[0]) if (grow and grow[0]) else 0
1112
+
1113
+ if not to_delete and not to_update:
1114
+ return {
1115
+ "status": "ok",
1116
+ "scanned": len(normalized_paths),
1117
+ "indexed": 0,
1118
+ "removed": 0,
1119
+ "reindexed": [],
1120
+ "deleted": [],
1121
+ "skipped": skipped,
1122
+ "parse_failed": [],
1123
+ "generation": current_gen,
1124
+ "elapsed_ms": round((time.perf_counter() - t0) * 1000.0, 2),
1125
+ }
1126
+
1127
+ # 3. Apply atomic updates
1128
+ self.governor.set_indexing_active(True)
1129
+ reindexed: list[str] = []
1130
+ parse_failed: list[str] = []
1131
+
1132
+ for del_path in to_delete:
1133
+ self._delete_file(con, del_path)
1134
+
1135
+ for rel_path, lang, content, digest, is_existing in to_update:
1136
+ cat_info = classify_file_detailed(Path(rel_path), content)
1137
+ success = self._replace_file(
1138
+ con,
1139
+ rel_path,
1140
+ lang,
1141
+ content,
1142
+ digest,
1143
+ category=cat_info.category.value,
1144
+ is_existing=is_existing,
1145
+ )
1146
+ if success:
1147
+ reindexed.append(rel_path)
1148
+ else:
1149
+ parse_failed.append(rel_path)
1150
+
1151
+ # 4. Run reference resolution pass
1152
+ head_commit = current_commit(self.repository)
1153
+ now_ts = int(time.time())
1154
+
1155
+ dirty_set = set(reindexed)
1156
+ removed_set = set(to_delete)
1157
+
1158
+ self._run_global_resolution(
1159
+ con,
1160
+ head_commit,
1161
+ dirty_files=dirty_set,
1162
+ removed_files=removed_set,
1163
+ )
1164
+
1165
+ from codegraph.async_queue import AsyncQueueScanner
1166
+
1167
+ AsyncQueueScanner(self.repository).scan_repository(con)
1168
+
1169
+ # Invalidate context cache on index update
1170
+ try:
1171
+ con.execute("DELETE FROM context_cache")
1172
+ except sqlite3.OperationalError:
1173
+ pass
1174
+
1175
+ # 5. Advance generation and update metadata
1176
+ next_gen = current_gen + 1
1177
+ con.executemany(
1178
+ "INSERT INTO metadata(key, value) VALUES(?,?) "
1179
+ "ON CONFLICT(key) DO UPDATE SET value=excluded.value",
1180
+ [
1181
+ ("repository", str(self.repository)),
1182
+ ("parser_version", PARSER_VERSION),
1183
+ ("schema_version", str(SCHEMA_VERSION)),
1184
+ ("index_generation", str(next_gen)),
1185
+ ("index_timestamp", str(now_ts)),
1186
+ ("resolution_dirty", "0"),
1187
+ ("index_status", "ok"),
1188
+ ],
1189
+ )
1190
+ save_commit(con, head_commit, now_ts)
1191
+ con.commit()
1192
+ try:
1193
+ con.execute("PRAGMA wal_checkpoint(PASSIVE)")
1194
+ except sqlite3.OperationalError:
1195
+ pass
1196
+ except Exception:
1197
+ con.rollback()
1198
+ raise
1199
+ finally:
1200
+ self.governor.set_indexing_active(False)
1201
+ con.close()
1202
+
1203
+ # Invalidate in-memory caches and reset pool
1204
+ get_parse_cache().clear()
1205
+ get_graph_cache().clear()
1206
+ self.reset_pool()
1207
+
1208
+ elapsed_ms = round((time.perf_counter() - t0) * 1000.0, 2)
1209
+ return {
1210
+ "status": "ok",
1211
+ "scanned": len(normalized_paths),
1212
+ "indexed": len(reindexed),
1213
+ "removed": len(to_delete),
1214
+ "reindexed": reindexed,
1215
+ "deleted": to_delete,
1216
+ "skipped": skipped,
1217
+ "parse_failed": parse_failed,
1218
+ "generation": next_gen,
1219
+ "elapsed_ms": elapsed_ms,
1220
+ }
1221
+
973
1222
  def _delete_file(
974
1223
  self,
975
1224
  con: sqlite3.Connection,
@@ -1662,7 +1911,10 @@ class Indexer:
1662
1911
  from codegraph.monorepo import detect_workspace
1663
1912
  workspace = detect_workspace(self.repository, con=con)
1664
1913
 
1665
- # Run resolution engine with composed routes, mount edges, bindings, and workspace
1914
+ from codegraph.tsconfig import TsConfigResolver
1915
+ tsconfig_resolver = TsConfigResolver.load_from_repo(self.repository)
1916
+
1917
+ # Run resolution engine with composed routes, mount edges, bindings, workspace, and tsconfig
1666
1918
  resolver = ReferenceResolver(
1667
1919
  symbols=symbols,
1668
1920
  imports=imports,
@@ -1675,6 +1927,7 @@ class Indexer:
1675
1927
  indexed_commit=indexed_commit,
1676
1928
  bindings=bindings,
1677
1929
  workspace=workspace,
1930
+ tsconfig_resolver=tsconfig_resolver,
1678
1931
  max_reexport_depth=self.max_reexport_depth,
1679
1932
  max_wildcard_expansions=self.max_wildcard_expansions,
1680
1933
  )
@@ -30,6 +30,12 @@ _SOURCE_EXTENSIONS = (
30
30
  ".py",
31
31
  ".ts",
32
32
  ".js",
33
+ ".html",
34
+ ".htm",
35
+ ".jinja",
36
+ ".jinja2",
37
+ ".njk",
38
+ ".ejs",
33
39
  )
34
40
 
35
41