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.
- codegraph/__init__.py +37 -0
- codegraph/agent.py +26 -0
- codegraph/architecture.py +328 -0
- codegraph/audit.py +106 -0
- codegraph/cache.py +95 -0
- codegraph/cli.py +854 -0
- codegraph/config.py +43 -0
- codegraph/constraints.py +238 -0
- codegraph/context.py +1228 -0
- codegraph/epistemic.py +90 -0
- codegraph/errors.py +275 -0
- codegraph/evidence/__init__.py +15 -0
- codegraph/evidence/citations.py +397 -0
- codegraph/frameworks.py +434 -0
- codegraph/freshness.py +295 -0
- codegraph/git.py +278 -0
- codegraph/graph/__init__.py +46 -0
- codegraph/graph/models.py +41 -0
- codegraph/graph/traversal.py +1291 -0
- codegraph/indexing/__init__.py +4 -0
- codegraph/indexing/classifier.py +274 -0
- codegraph/indexing/indexer.py +943 -0
- codegraph/indexing/models.py +338 -0
- codegraph/indexing/parser.py +1240 -0
- codegraph/indexing/scanner.py +200 -0
- codegraph/indexing/test_framework.py +116 -0
- codegraph/interrogation.py +1582 -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 +736 -0
- codegraph/memory/__init__.py +3 -0
- codegraph/memory/store.py +46 -0
- codegraph/models.py +289 -0
- codegraph/observability.py +151 -0
- codegraph/optimizer.py +372 -0
- codegraph/planner.py +417 -0
- codegraph/py.typed +1 -0
- codegraph/query_expansion.py +199 -0
- codegraph/ranking.py +363 -0
- codegraph/resolver.py +843 -0
- codegraph/resources/__init__.py +45 -0
- codegraph/resources/cache.py +117 -0
- codegraph/resources/coalescer.py +83 -0
- codegraph/resources/debouncer.py +98 -0
- codegraph/resources/governor.py +232 -0
- codegraph/resources/policy.py +123 -0
- codegraph/retrieval_policy.py +220 -0
- codegraph/search/__init__.py +23 -0
- codegraph/search/hybrid.py +301 -0
- codegraph/search/semantic.py +28 -0
- codegraph/security/__init__.py +3 -0
- codegraph/security/paths.py +35 -0
- codegraph/target_resolver.py +348 -0
- codegraph/task.py +637 -0
- codegraph_engine-2.1.1.dist-info/METADATA +334 -0
- codegraph_engine-2.1.1.dist-info/RECORD +62 -0
- codegraph_engine-2.1.1.dist-info/WHEEL +5 -0
- codegraph_engine-2.1.1.dist-info/entry_points.txt +2 -0
- codegraph_engine-2.1.1.dist-info/licenses/LICENSE +21 -0
- 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
|
+
)
|