codegraph-engine 2.1.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 (62) hide show
  1. codegraph/__init__.py +37 -0
  2. codegraph/agent.py +26 -0
  3. codegraph/architecture.py +328 -0
  4. codegraph/audit.py +106 -0
  5. codegraph/cache.py +95 -0
  6. codegraph/cli.py +854 -0
  7. codegraph/config.py +43 -0
  8. codegraph/constraints.py +238 -0
  9. codegraph/context.py +1228 -0
  10. codegraph/epistemic.py +90 -0
  11. codegraph/errors.py +275 -0
  12. codegraph/evidence/__init__.py +15 -0
  13. codegraph/evidence/citations.py +397 -0
  14. codegraph/frameworks.py +434 -0
  15. codegraph/freshness.py +295 -0
  16. codegraph/git.py +278 -0
  17. codegraph/graph/__init__.py +46 -0
  18. codegraph/graph/models.py +41 -0
  19. codegraph/graph/traversal.py +1291 -0
  20. codegraph/indexing/__init__.py +4 -0
  21. codegraph/indexing/classifier.py +274 -0
  22. codegraph/indexing/indexer.py +943 -0
  23. codegraph/indexing/models.py +338 -0
  24. codegraph/indexing/parser.py +1240 -0
  25. codegraph/indexing/scanner.py +200 -0
  26. codegraph/indexing/test_framework.py +116 -0
  27. codegraph/interrogation.py +1582 -0
  28. codegraph/llm/__init__.py +3 -0
  29. codegraph/llm/base.py +15 -0
  30. codegraph/llm/context.py +20 -0
  31. codegraph/mcp/__init__.py +3 -0
  32. codegraph/mcp/server.py +736 -0
  33. codegraph/memory/__init__.py +3 -0
  34. codegraph/memory/store.py +46 -0
  35. codegraph/models.py +289 -0
  36. codegraph/observability.py +151 -0
  37. codegraph/optimizer.py +372 -0
  38. codegraph/planner.py +417 -0
  39. codegraph/py.typed +1 -0
  40. codegraph/query_expansion.py +199 -0
  41. codegraph/ranking.py +363 -0
  42. codegraph/resolver.py +843 -0
  43. codegraph/resources/__init__.py +45 -0
  44. codegraph/resources/cache.py +117 -0
  45. codegraph/resources/coalescer.py +83 -0
  46. codegraph/resources/debouncer.py +98 -0
  47. codegraph/resources/governor.py +232 -0
  48. codegraph/resources/policy.py +123 -0
  49. codegraph/retrieval_policy.py +220 -0
  50. codegraph/search/__init__.py +23 -0
  51. codegraph/search/hybrid.py +301 -0
  52. codegraph/search/semantic.py +28 -0
  53. codegraph/security/__init__.py +3 -0
  54. codegraph/security/paths.py +35 -0
  55. codegraph/target_resolver.py +348 -0
  56. codegraph/task.py +637 -0
  57. codegraph_engine-2.1.1.dist-info/METADATA +334 -0
  58. codegraph_engine-2.1.1.dist-info/RECORD +62 -0
  59. codegraph_engine-2.1.1.dist-info/WHEEL +5 -0
  60. codegraph_engine-2.1.1.dist-info/entry_points.txt +2 -0
  61. codegraph_engine-2.1.1.dist-info/licenses/LICENSE +21 -0
  62. codegraph_engine-2.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,397 @@
1
+ """First-class Evidence Engine with deterministic source-hash verification.
2
+
3
+ Every evidence item contains:
4
+ evidence_id, repository, file, start_line, end_line, symbol, relationship,
5
+ source_hash, indexed_commit, confidence, evidence_type, evidence_status.
6
+
7
+ Before returning evidence, current file content is hashed and compared against
8
+ the stored source_hash. If modified or deleted since indexing, evidence_status
9
+ is set to 'stale' (never silently returning obsolete citations as current).
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import hashlib
14
+ import sqlite3
15
+ from dataclasses import asdict, dataclass
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ from codegraph.freshness import index_generation
20
+ from codegraph.security import safe_path
21
+
22
+ PARSER_VERSION = "3.0"
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class Evidence:
27
+ file: str
28
+ start_line: int
29
+ end_line: int
30
+ symbol: str | None
31
+ snippet: str
32
+ evidence_type: str = "source"
33
+ confidence: str = "HIGH"
34
+ source_hash: str | None = None
35
+ indexed_commit: str | None = None
36
+ evidence_status: str = "current" # current | stale | deleted | unknown
37
+ relationship: str = "DEFINES"
38
+ repository: str = ""
39
+ evidence_id: str = ""
40
+ start_column: int | None = None
41
+ end_column: int | None = None
42
+ index_generation: int = 0
43
+ parser_version: str = PARSER_VERSION
44
+
45
+ def __post_init__(self) -> None:
46
+ if not self.evidence_id:
47
+ raw = f"{self.file}:{self.start_line}:{self.end_line}:{self.symbol or ''}:{self.relationship}"
48
+ eid = "ev_" + hashlib.sha256(raw.encode("utf-8")).hexdigest()[:12]
49
+ object.__setattr__(self, "evidence_id", eid)
50
+
51
+ def as_dict(self) -> dict[str, object]:
52
+ return asdict(self)
53
+
54
+ def __getitem__(self, key: str) -> object:
55
+ return self.as_dict()[key]
56
+
57
+ def get(self, key: str, default: object = None) -> object:
58
+ return self.as_dict().get(key, default)
59
+
60
+
61
+ def get_repository_from_con(con: sqlite3.Connection) -> Path | None:
62
+ """Retrieve the repository root path stored in the index metadata table."""
63
+ try:
64
+ row = con.execute(
65
+ "SELECT value FROM metadata WHERE key='repository'"
66
+ ).fetchone()
67
+ if row and row[0]:
68
+ p = Path(str(row[0]))
69
+ if p.exists():
70
+ return p
71
+ except sqlite3.OperationalError:
72
+ pass
73
+ return None
74
+
75
+
76
+ def verify_source_hash(
77
+ repository: Path | None,
78
+ file_path: str,
79
+ stored_hash: str | None,
80
+ ) -> str:
81
+ """Verify whether the current file on disk matches `stored_hash`.
82
+
83
+ Returns 'current', 'stale', or 'deleted'.
84
+ """
85
+ if not stored_hash or repository is None:
86
+ return "current"
87
+ abs_path = repository / file_path
88
+ if not abs_path.exists():
89
+ return "stale"
90
+ try:
91
+ current_digest = hashlib.sha256(
92
+ abs_path.read_text(encoding="utf-8", errors="replace").encode()
93
+ ).hexdigest()
94
+ return "current" if current_digest == stored_hash else "stale"
95
+ except OSError:
96
+ return "stale"
97
+
98
+
99
+ def build_evidence(
100
+ con: sqlite3.Connection,
101
+ repository: Path | None,
102
+ path: str,
103
+ symbol: str | None = None,
104
+ limit: int = 50,
105
+ ) -> list[Evidence]:
106
+ """Build verified evidence list for a file/symbol, checking source hash freshness."""
107
+ from codegraph.freshness import indexed_commit as get_indexed_commit
108
+
109
+ repo = repository or get_repository_from_con(con)
110
+ idx_commit = get_indexed_commit(con)
111
+ repo_str = str(repo) if repo else ""
112
+
113
+ # Look up the file-level hash in `files` table for source verification
114
+ file_row = con.execute(
115
+ "SELECT hash FROM files WHERE path=?", (path,)
116
+ ).fetchone()
117
+ file_hash = str(file_row["hash"]) if file_row else None
118
+
119
+ if symbol:
120
+ short = symbol.split(".")[-1]
121
+ rows = con.execute(
122
+ "SELECT symbol, start_line, end_line, content, hash FROM chunks "
123
+ "WHERE path=? AND (symbol=? OR symbol LIKE ? OR symbol=?) LIMIT ?",
124
+ (path, symbol, f"%.{symbol}", short, limit),
125
+ ).fetchall()
126
+ else:
127
+ rows = con.execute(
128
+ "SELECT symbol, start_line, end_line, content, hash FROM chunks "
129
+ "WHERE path=? ORDER BY start_line LIMIT ?",
130
+ (path, limit),
131
+ ).fetchall()
132
+
133
+ results: list[Evidence] = []
134
+ for row in rows:
135
+ expected_hash = file_hash or str(row["hash"])
136
+ status = verify_source_hash(repo, path, expected_hash)
137
+ confidence = "HIGH" if status == "current" else "LOW"
138
+ results.append(
139
+ Evidence(
140
+ file=path,
141
+ start_line=int(row["start_line"]),
142
+ end_line=int(row["end_line"]),
143
+ symbol=row["symbol"],
144
+ snippet=str(row["content"])[:900],
145
+ evidence_type="source",
146
+ confidence=confidence,
147
+ source_hash=expected_hash,
148
+ indexed_commit=idx_commit,
149
+ evidence_status=status,
150
+ relationship="DEFINES",
151
+ repository=repo_str,
152
+ )
153
+ )
154
+ return results
155
+
156
+
157
+ @dataclass
158
+ class VerificationReport:
159
+ valid: bool
160
+ exists: bool
161
+ hash_matches: bool
162
+ lines_valid: bool
163
+ status: str # FACT | CONFLICT | UNKNOWN
164
+ confidence: str # HIGH | MEDIUM | LOW | UNKNOWN
165
+ freshness: str # FRESH | STALE | UNKNOWN
166
+ reason: str
167
+ file: str
168
+ start_line: int | None = None
169
+ end_line: int | None = None
170
+ symbol: str | None = None
171
+ evidence_id: str | None = None
172
+ index_generation: int = 0
173
+ parser_version: str = PARSER_VERSION
174
+
175
+ def as_dict(self) -> dict[str, object]:
176
+ d: dict[str, object] = asdict(self)
177
+ d["epistemic_status"] = self.status
178
+ return d
179
+
180
+ def __getitem__(self, key: str) -> Any:
181
+ return self.as_dict()[key]
182
+
183
+ def get(self, key: str, default: Any = None) -> Any:
184
+ return self.as_dict().get(key, default)
185
+
186
+
187
+ def verify_evidence(
188
+ con: sqlite3.Connection,
189
+ repository: Path,
190
+ evidence: Evidence | dict[str, Any] | str | None = None,
191
+ evidence_id: str | None = None,
192
+ file_path: str | None = None,
193
+ start_line: int | None = None,
194
+ end_line: int | None = None,
195
+ symbol: str | None = None,
196
+ expected_content_hash: str | None = None,
197
+ expected_hash: str | None = None,
198
+ ) -> VerificationReport:
199
+ """Verify validity and freshness of an evidence reference against current repository disk state."""
200
+ gen = index_generation(con)
201
+
202
+ resolved_file = file_path
203
+ resolved_start = start_line
204
+ resolved_end = end_line
205
+ resolved_symbol = symbol
206
+ resolved_id = evidence_id
207
+ expected_h = expected_content_hash or expected_hash
208
+
209
+ if isinstance(evidence, Evidence):
210
+ resolved_file = evidence.file
211
+ resolved_start = evidence.start_line
212
+ resolved_end = evidence.end_line
213
+ resolved_symbol = evidence.symbol
214
+ resolved_id = evidence.evidence_id
215
+ elif isinstance(evidence, dict):
216
+ resolved_file = str(evidence.get("file") or evidence.get("file_path") or "")
217
+ resolved_start = evidence.get("start_line")
218
+ resolved_end = evidence.get("end_line")
219
+ resolved_symbol = evidence.get("symbol")
220
+ resolved_id = evidence.get("evidence_id")
221
+ if not expected_h:
222
+ expected_h = evidence.get("source_hash")
223
+ elif isinstance(evidence, str):
224
+ if "/" in evidence or evidence.endswith(".py") or evidence.endswith(".ts") or evidence.endswith(".js"):
225
+ resolved_file = evidence
226
+ else:
227
+ resolved_id = evidence
228
+
229
+ if resolved_id and not resolved_file:
230
+ edge_row = con.execute(
231
+ "SELECT file, start_line, end_line, source, target, relationship FROM graph_edges WHERE evidence_id=?",
232
+ (resolved_id,),
233
+ ).fetchone()
234
+ if edge_row:
235
+ resolved_file = str(edge_row["file"])
236
+ resolved_start = int(edge_row["start_line"])
237
+ resolved_end = int(edge_row["end_line"])
238
+ if not resolved_symbol:
239
+ resolved_symbol = str(edge_row["target"])
240
+
241
+ if not resolved_file:
242
+ return VerificationReport(
243
+ valid=False,
244
+ exists=False,
245
+ hash_matches=False,
246
+ lines_valid=False,
247
+ status="UNKNOWN",
248
+ confidence="UNKNOWN",
249
+ freshness="UNKNOWN",
250
+ reason="Missing file path and evidence_id not found in index",
251
+ file="",
252
+ index_generation=gen,
253
+ )
254
+
255
+ try:
256
+ abs_path = safe_path(repository, resolved_file)
257
+ except Exception as e:
258
+ return VerificationReport(
259
+ valid=False,
260
+ exists=False,
261
+ hash_matches=False,
262
+ lines_valid=False,
263
+ status="UNKNOWN",
264
+ confidence="UNKNOWN",
265
+ freshness="UNKNOWN",
266
+ reason=f"Repository boundary violation: {e}",
267
+ file=resolved_file,
268
+ index_generation=gen,
269
+ )
270
+
271
+ if not abs_path.exists() or not abs_path.is_file():
272
+ return VerificationReport(
273
+ valid=False,
274
+ exists=False,
275
+ hash_matches=False,
276
+ lines_valid=False,
277
+ status="UNKNOWN",
278
+ confidence="LOW",
279
+ freshness="STALE",
280
+ reason=f"File not found on disk: {resolved_file}",
281
+ file=resolved_file,
282
+ index_generation=gen,
283
+ )
284
+
285
+ try:
286
+ content_bytes = abs_path.read_bytes()
287
+ current_hash = hashlib.sha256(content_bytes).hexdigest()
288
+ except OSError as e:
289
+ return VerificationReport(
290
+ valid=False,
291
+ exists=True,
292
+ hash_matches=False,
293
+ lines_valid=False,
294
+ status="CONFLICT",
295
+ confidence="LOW",
296
+ freshness="STALE",
297
+ reason=f"Could not read file from disk: {e}",
298
+ file=resolved_file,
299
+ index_generation=gen,
300
+ )
301
+
302
+ # Check against expected hash if supplied
303
+ if expected_h and current_hash != expected_h:
304
+ return VerificationReport(
305
+ valid=False,
306
+ exists=True,
307
+ hash_matches=False,
308
+ lines_valid=False,
309
+ status="CONFLICT",
310
+ confidence="LOW",
311
+ freshness="STALE",
312
+ reason=f"Source hash mismatch (expected {expected_h[:8]}..., found {current_hash[:8]}...)",
313
+ file=resolved_file,
314
+ start_line=resolved_start,
315
+ end_line=resolved_end,
316
+ symbol=resolved_symbol,
317
+ index_generation=gen,
318
+ )
319
+
320
+ # Check against indexed database hash if available
321
+ file_row = con.execute("SELECT hash FROM files WHERE path=?", (resolved_file,)).fetchone()
322
+ if file_row:
323
+ stored_hash = str(file_row["hash"])
324
+ if current_hash != stored_hash:
325
+ return VerificationReport(
326
+ valid=False,
327
+ exists=True,
328
+ hash_matches=False,
329
+ lines_valid=False,
330
+ status="CONFLICT",
331
+ confidence="LOW",
332
+ freshness="STALE",
333
+ reason="Source hash changed (file modified on disk after indexing)",
334
+ file=resolved_file,
335
+ start_line=resolved_start,
336
+ end_line=resolved_end,
337
+ symbol=resolved_symbol,
338
+ index_generation=gen,
339
+ )
340
+
341
+ lines = content_bytes.splitlines()
342
+ line_count = len(lines)
343
+ if resolved_start is not None and resolved_end is not None:
344
+ if resolved_start < 1 or resolved_end > line_count or resolved_start > resolved_end:
345
+ return VerificationReport(
346
+ valid=False,
347
+ exists=True,
348
+ hash_matches=True,
349
+ lines_valid=False,
350
+ status="CONFLICT",
351
+ confidence="LOW",
352
+ freshness="STALE",
353
+ reason=f"Referenced line range {resolved_start}-{resolved_end} exceeds current file length ({line_count} lines)",
354
+ file=resolved_file,
355
+ start_line=resolved_start,
356
+ end_line=resolved_end,
357
+ symbol=resolved_symbol,
358
+ index_generation=gen,
359
+ )
360
+
361
+ if resolved_symbol:
362
+ sym_row = con.execute(
363
+ "SELECT canonical_id FROM symbols WHERE path=? AND (name=? OR qualified_name=? OR canonical_id=?)",
364
+ (resolved_file, resolved_symbol, resolved_symbol, resolved_symbol),
365
+ ).fetchone()
366
+ short_sym = resolved_symbol.split(".")[-1]
367
+ if not sym_row and short_sym not in abs_path.read_text(encoding="utf-8", errors="replace"):
368
+ return VerificationReport(
369
+ valid=False,
370
+ exists=True,
371
+ hash_matches=True,
372
+ lines_valid=True,
373
+ status="UNKNOWN",
374
+ confidence="LOW",
375
+ freshness="STALE",
376
+ reason=f"Referenced symbol '{resolved_symbol}' no longer found in file",
377
+ file=resolved_file,
378
+ symbol=resolved_symbol,
379
+ index_generation=gen,
380
+ )
381
+
382
+ return VerificationReport(
383
+ valid=True,
384
+ exists=True,
385
+ hash_matches=True,
386
+ lines_valid=True,
387
+ status="FACT",
388
+ confidence="HIGH",
389
+ freshness="FRESH",
390
+ reason="Evidence verified against source file and disk hash",
391
+ file=resolved_file,
392
+ start_line=resolved_start,
393
+ end_line=resolved_end,
394
+ symbol=resolved_symbol,
395
+ evidence_id=resolved_id,
396
+ index_generation=gen,
397
+ )