codegraph-engine 2.1.2__py3-none-any.whl → 2.1.7__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 +1 -1
- codegraph/agent_brain.py +1961 -0
- codegraph/agent_capabilities.py +3500 -0
- codegraph/agent_rules.py +530 -0
- codegraph/architecture.py +48 -5
- codegraph/binding_resolver.py +616 -0
- codegraph/cli.py +345 -5
- codegraph/config.py +2 -0
- codegraph/context.py +845 -93
- codegraph/database/__init__.py +58 -0
- codegraph/database/extractor.py +2956 -0
- codegraph/database/interrogation.py +1085 -0
- codegraph/database/models.py +310 -0
- codegraph/epistemic.py +95 -0
- codegraph/errors.py +7 -0
- codegraph/evidence/citations.py +22 -1
- codegraph/evidence_contract.py +455 -0
- codegraph/frameworks.py +372 -11
- codegraph/git.py +92 -10
- codegraph/graph/models.py +15 -0
- codegraph/graph/traversal.py +680 -121
- codegraph/indexing/__init__.py +15 -1
- codegraph/indexing/classifier.py +343 -58
- codegraph/indexing/indexer.py +1515 -141
- codegraph/indexing/models.py +75 -0
- codegraph/indexing/parser.py +1843 -70
- codegraph/indexing/scanner.py +4 -1
- codegraph/indexing/telemetry.py +326 -0
- codegraph/installer.py +1652 -0
- codegraph/interrogation.py +708 -122
- codegraph/mcp/server.py +525 -107
- codegraph/mcp_diagnostics.py +879 -0
- codegraph/models.py +14 -0
- codegraph/monorepo.py +922 -0
- codegraph/observability.py +113 -2
- codegraph/optimizer.py +647 -42
- codegraph/ranking.py +61 -16
- codegraph/resolver.py +2006 -77
- codegraph/resources/monitor.py +1 -1
- codegraph/retrieval_policy.py +96 -17
- codegraph/route_composer.py +363 -0
- codegraph/runtime/__init__.py +32 -0
- codegraph/runtime/ingestor.py +672 -0
- codegraph/runtime/models.py +130 -0
- codegraph/runtime/reconciliation.py +492 -0
- codegraph/search/__init__.py +2 -0
- codegraph/search/hybrid.py +572 -53
- codegraph/security/__init__.py +50 -2
- codegraph/security/paths.py +162 -8
- codegraph/security/redaction.py +633 -0
- codegraph/semantic_decorators.py +341 -0
- codegraph/target_resolver.py +6 -6
- codegraph/tool_selection_eval.py +1158 -0
- codegraph_engine-2.1.7.dist-info/METADATA +617 -0
- codegraph_engine-2.1.7.dist-info/RECORD +84 -0
- {codegraph_engine-2.1.2.dist-info → codegraph_engine-2.1.7.dist-info}/licenses/LICENSE +1 -1
- codegraph_engine-2.1.2.dist-info/METADATA +0 -336
- codegraph_engine-2.1.2.dist-info/RECORD +0 -63
- {codegraph_engine-2.1.2.dist-info → codegraph_engine-2.1.7.dist-info}/WHEEL +0 -0
- {codegraph_engine-2.1.2.dist-info → codegraph_engine-2.1.7.dist-info}/entry_points.txt +0 -0
- {codegraph_engine-2.1.2.dist-info → codegraph_engine-2.1.7.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Canonical Runtime Observation & Static-Runtime Reconciliation Models (Phases 10-13, 16).
|
|
2
|
+
|
|
3
|
+
Architectural Rules:
|
|
4
|
+
1. Runtime mapping is an OPTIONAL observation layer.
|
|
5
|
+
2. Static and runtime evidence remain strictly distinguishable (`evidence_class="RUNTIME_OBSERVED"`).
|
|
6
|
+
3. Never convert runtime evidence into static proof (`AST_VERIFIED`).
|
|
7
|
+
4. Never convert static inference into runtime fact.
|
|
8
|
+
5. `NOT_OBSERVED_AT_RUNTIME` means no runtime observation was ingested for the static edge;
|
|
9
|
+
it NEVER means the path cannot happen.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from enum import StrEnum
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from codegraph.security.redaction import normalize_and_redact_sql, redact_payload, redact_secrets
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ReconciliationStatus(StrEnum):
|
|
21
|
+
CONFIRMED_RUNTIME_PATH = "CONFIRMED_RUNTIME_PATH"
|
|
22
|
+
STATIC_RUNTIME_CONFLICT = "STATIC_RUNTIME_CONFLICT"
|
|
23
|
+
NOT_OBSERVED_AT_RUNTIME = "NOT_OBSERVED_AT_RUNTIME"
|
|
24
|
+
RUNTIME_ONLY_OBSERVED = "RUNTIME_ONLY_OBSERVED"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class RuntimeEvent:
|
|
29
|
+
"""Normalized, privacy-safe runtime observation event."""
|
|
30
|
+
|
|
31
|
+
event_id: str
|
|
32
|
+
trace_id: str
|
|
33
|
+
span_id: str
|
|
34
|
+
parent_span_id: str = ""
|
|
35
|
+
timestamp: str = ""
|
|
36
|
+
source_format: str = "json" # otel | json | sql_log
|
|
37
|
+
http_method: str = ""
|
|
38
|
+
route_path: str = ""
|
|
39
|
+
handler_symbol: str = ""
|
|
40
|
+
caller_symbol: str = ""
|
|
41
|
+
callee_symbol: str = ""
|
|
42
|
+
service_name: str = ""
|
|
43
|
+
db_operation: str = "" # SELECT | INSERT | UPDATE | DELETE | ""
|
|
44
|
+
db_table: str = ""
|
|
45
|
+
db_columns: tuple[str, ...] = ()
|
|
46
|
+
normalized_sql: str = ""
|
|
47
|
+
status_code: int | None = None
|
|
48
|
+
exception_type: str = ""
|
|
49
|
+
duration_ms: float = 0.0
|
|
50
|
+
evidence_class: str = "RUNTIME_OBSERVED"
|
|
51
|
+
runtime_generation: int = 1
|
|
52
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
53
|
+
|
|
54
|
+
def __post_init__(self) -> None:
|
|
55
|
+
if self.normalized_sql:
|
|
56
|
+
object.__setattr__(self, "normalized_sql", normalize_and_redact_sql(self.normalized_sql))
|
|
57
|
+
if self.metadata:
|
|
58
|
+
object.__setattr__(self, "metadata", redact_payload(dict(self.metadata)))
|
|
59
|
+
|
|
60
|
+
def as_dict(self) -> dict[str, Any]:
|
|
61
|
+
return {
|
|
62
|
+
"event_id": self.event_id,
|
|
63
|
+
"trace_id": self.trace_id,
|
|
64
|
+
"span_id": self.span_id,
|
|
65
|
+
"parent_span_id": self.parent_span_id,
|
|
66
|
+
"timestamp": self.timestamp,
|
|
67
|
+
"source_format": self.source_format,
|
|
68
|
+
"http_method": self.http_method,
|
|
69
|
+
"route_path": redact_secrets(self.route_path),
|
|
70
|
+
"handler_symbol": self.handler_symbol,
|
|
71
|
+
"caller_symbol": self.caller_symbol,
|
|
72
|
+
"callee_symbol": self.callee_symbol,
|
|
73
|
+
"service_name": self.service_name,
|
|
74
|
+
"db_operation": self.db_operation,
|
|
75
|
+
"db_table": self.db_table,
|
|
76
|
+
"db_columns": list(self.db_columns),
|
|
77
|
+
"normalized_sql": self.normalized_sql,
|
|
78
|
+
"status_code": self.status_code,
|
|
79
|
+
"exception_type": redact_secrets(self.exception_type),
|
|
80
|
+
"duration_ms": self.duration_ms,
|
|
81
|
+
"evidence_class": "RUNTIME_OBSERVED",
|
|
82
|
+
"runtime_generation": self.runtime_generation,
|
|
83
|
+
"metadata": dict(self.metadata),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True)
|
|
88
|
+
class RuntimeAggregatedEdge:
|
|
89
|
+
"""Compact, deduplicated runtime edge stored in SQLite (Phase 16)."""
|
|
90
|
+
|
|
91
|
+
edge_id: str
|
|
92
|
+
source: str
|
|
93
|
+
target: str
|
|
94
|
+
relationship: str # HANDLED_BY | CALLS | READS_TABLE | WRITES_TABLE | READS_COLUMN | WRITES_COLUMN
|
|
95
|
+
operation: str = ""
|
|
96
|
+
evidence_class: str = "RUNTIME_OBSERVED"
|
|
97
|
+
observation_count: int = 1
|
|
98
|
+
first_seen: str = ""
|
|
99
|
+
last_seen: str = ""
|
|
100
|
+
avg_duration_ms: float = 0.0
|
|
101
|
+
max_duration_ms: float = 0.0
|
|
102
|
+
sample_trace_id: str = ""
|
|
103
|
+
sample_span_id: str = ""
|
|
104
|
+
normalized_sql: str = ""
|
|
105
|
+
status_code: int | None = None
|
|
106
|
+
exception_type: str = ""
|
|
107
|
+
runtime_generation: int = 1
|
|
108
|
+
|
|
109
|
+
def as_dict(self) -> dict[str, Any]:
|
|
110
|
+
return {
|
|
111
|
+
"edge_id": self.edge_id,
|
|
112
|
+
"source": self.source,
|
|
113
|
+
"target": self.target,
|
|
114
|
+
"relationship": self.relationship,
|
|
115
|
+
"operation": self.operation,
|
|
116
|
+
"evidence_class": "RUNTIME_OBSERVED",
|
|
117
|
+
"confidence": "HIGH",
|
|
118
|
+
"status": "RUNTIME_OBSERVED",
|
|
119
|
+
"observation_count": self.observation_count,
|
|
120
|
+
"first_seen": self.first_seen,
|
|
121
|
+
"last_seen": self.last_seen,
|
|
122
|
+
"avg_duration_ms": round(self.avg_duration_ms, 3),
|
|
123
|
+
"max_duration_ms": round(self.max_duration_ms, 3),
|
|
124
|
+
"sample_trace_id": self.sample_trace_id,
|
|
125
|
+
"sample_span_id": self.sample_span_id,
|
|
126
|
+
"normalized_sql": self.normalized_sql,
|
|
127
|
+
"status_code": self.status_code,
|
|
128
|
+
"exception_type": self.exception_type,
|
|
129
|
+
"runtime_generation": self.runtime_generation,
|
|
130
|
+
}
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
"""Runtime Map & Static-vs-Runtime Reconciliation Engine (Phases 12 & 13).
|
|
2
|
+
|
|
3
|
+
Enables CodeGraph to:
|
|
4
|
+
1. Query the canonical runtime graph (`HTTP -> ROUTE -> HANDLER -> FUNCTION -> SERVICE -> DB OP -> TABLE -> COLUMN`)
|
|
5
|
+
and reverse runtime mappings (`TABLE -> WRITERS`, `TABLE -> READERS`, `ROUTE -> DATABASE`,
|
|
6
|
+
`DATABASE -> ROUTES`, `FUNCTION -> QUERIES`, `QUERY -> SOURCE LOCATION`).
|
|
7
|
+
2. Compare static code+database expectations against runtime observations:
|
|
8
|
+
- `CONFIRMED_RUNTIME_PATH`
|
|
9
|
+
- `STATIC_RUNTIME_CONFLICT`
|
|
10
|
+
- `NOT_OBSERVED_AT_RUNTIME` (observational only; never implies impossibility)
|
|
11
|
+
- `RUNTIME_ONLY_OBSERVED` (never promoted to static `AST_VERIFIED`)
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import sqlite3
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from codegraph.security.redaction import redact_payload, redact_secrets
|
|
20
|
+
|
|
21
|
+
from .models import ReconciliationStatus
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_runtime_trace(
|
|
25
|
+
con: sqlite3.Connection,
|
|
26
|
+
repo_or_trace_id: Any = None,
|
|
27
|
+
route: str | None = None,
|
|
28
|
+
symbol: str | None = None,
|
|
29
|
+
table: str | None = None,
|
|
30
|
+
*,
|
|
31
|
+
trace_id: str | None = None,
|
|
32
|
+
) -> dict[str, Any]:
|
|
33
|
+
"""Query runtime observations and the aggregated runtime graph (Phase 13)."""
|
|
34
|
+
if isinstance(repo_or_trace_id, str) and not trace_id:
|
|
35
|
+
trace_id = repo_or_trace_id
|
|
36
|
+
obs_rows = con.execute(
|
|
37
|
+
"SELECT event_id, trace_id, span_id, parent_span_id, timestamp, source_format, "
|
|
38
|
+
"http_method, route_path, handler_symbol, caller_symbol, callee_symbol, service_name, "
|
|
39
|
+
"db_operation, db_table, db_columns_json, normalized_sql, status_code, exception_type, "
|
|
40
|
+
"duration_ms, evidence_class, runtime_generation "
|
|
41
|
+
"FROM runtime_observations ORDER BY runtime_generation DESC, event_id ASC"
|
|
42
|
+
).fetchall()
|
|
43
|
+
|
|
44
|
+
edge_rows = con.execute(
|
|
45
|
+
"SELECT edge_id, source, target, relationship, operation, evidence_class, "
|
|
46
|
+
"observation_count, first_seen, last_seen, avg_duration_ms, max_duration_ms, "
|
|
47
|
+
"sample_trace_id, sample_span_id, normalized_sql, status_code, exception_type, runtime_generation "
|
|
48
|
+
"FROM runtime_edges ORDER BY observation_count DESC, edge_id ASC"
|
|
49
|
+
).fetchall()
|
|
50
|
+
|
|
51
|
+
q_trace = (trace_id or "").strip()
|
|
52
|
+
q_route = (route or "").strip().lower()
|
|
53
|
+
q_sym = (symbol or "").strip().lower()
|
|
54
|
+
q_tbl = (table or "").strip().lower().split(".")[-1]
|
|
55
|
+
|
|
56
|
+
# Pre-collect trace_ids that match q_route so child spans in the same trace are retained
|
|
57
|
+
route_matched_traces: set[str] = set()
|
|
58
|
+
if q_route:
|
|
59
|
+
for r in obs_rows:
|
|
60
|
+
if q_route in str(r["route_path"]).lower():
|
|
61
|
+
route_matched_traces.add(str(r["trace_id"]))
|
|
62
|
+
|
|
63
|
+
filtered_obs: list[dict[str, Any]] = []
|
|
64
|
+
for r in obs_rows:
|
|
65
|
+
if q_trace and str(r["trace_id"]) != q_trace:
|
|
66
|
+
continue
|
|
67
|
+
if (
|
|
68
|
+
q_route
|
|
69
|
+
and q_route not in str(r["route_path"]).lower()
|
|
70
|
+
and str(r["trace_id"]) not in route_matched_traces
|
|
71
|
+
):
|
|
72
|
+
continue
|
|
73
|
+
if q_sym and not any(
|
|
74
|
+
q_sym in str(r[col]).lower()
|
|
75
|
+
for col in ("handler_symbol", "caller_symbol", "callee_symbol", "service_name")
|
|
76
|
+
):
|
|
77
|
+
continue
|
|
78
|
+
if q_tbl and q_tbl != str(r["db_table"]).lower():
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
# Attempt to map query back to static source location if symbol is known
|
|
82
|
+
caller_sym = str(r["caller_symbol"] or r["handler_symbol"] or "")
|
|
83
|
+
src_loc: dict[str, Any] | None = None
|
|
84
|
+
if caller_sym:
|
|
85
|
+
srow = con.execute(
|
|
86
|
+
"SELECT path, start_line, end_line FROM symbols WHERE canonical_id = ? OR qualified_name = ? OR name = ? LIMIT 1",
|
|
87
|
+
(caller_sym, caller_sym, caller_sym.split(".")[-1]),
|
|
88
|
+
).fetchone()
|
|
89
|
+
if srow:
|
|
90
|
+
src_loc = {
|
|
91
|
+
"file": str(srow["path"]),
|
|
92
|
+
"start_line": int(srow["start_line"]),
|
|
93
|
+
"end_line": int(srow["end_line"]),
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
filtered_obs.append({
|
|
97
|
+
"event_id": str(r["event_id"]),
|
|
98
|
+
"trace_id": str(r["trace_id"]),
|
|
99
|
+
"span_id": str(r["span_id"]),
|
|
100
|
+
"parent_span_id": str(r["parent_span_id"]),
|
|
101
|
+
"timestamp": str(r["timestamp"]),
|
|
102
|
+
"source_format": str(r["source_format"]),
|
|
103
|
+
"http_method": str(r["http_method"]),
|
|
104
|
+
"route_path": redact_secrets(str(r["route_path"])),
|
|
105
|
+
"handler_symbol": str(r["handler_symbol"]),
|
|
106
|
+
"caller_symbol": caller_sym,
|
|
107
|
+
"callee_symbol": str(r["callee_symbol"]),
|
|
108
|
+
"service_name": str(r["service_name"]),
|
|
109
|
+
"db_operation": str(r["db_operation"]),
|
|
110
|
+
"db_table": str(r["db_table"]),
|
|
111
|
+
"db_columns": json.loads(r["db_columns_json"] or "[]"),
|
|
112
|
+
"normalized_sql": redact_secrets(str(r["normalized_sql"])),
|
|
113
|
+
"status_code": r["status_code"],
|
|
114
|
+
"exception_type": redact_secrets(str(r["exception_type"])),
|
|
115
|
+
"duration_ms": float(r["duration_ms"] or 0.0),
|
|
116
|
+
"evidence_class": "RUNTIME_OBSERVED",
|
|
117
|
+
"runtime_generation": int(r["runtime_generation"]),
|
|
118
|
+
"source_location": src_loc,
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
filtered_edges: list[dict[str, Any]] = []
|
|
122
|
+
table_writers: dict[str, list[str]] = {}
|
|
123
|
+
table_readers: dict[str, list[str]] = {}
|
|
124
|
+
route_to_tables: dict[str, list[str]] = {}
|
|
125
|
+
table_to_routes: dict[str, list[str]] = {}
|
|
126
|
+
function_to_queries: dict[str, list[str]] = {}
|
|
127
|
+
|
|
128
|
+
for er in edge_rows:
|
|
129
|
+
src = str(er["source"])
|
|
130
|
+
tgt = str(er["target"])
|
|
131
|
+
rel = str(er["relationship"])
|
|
132
|
+
op = str(er["operation"])
|
|
133
|
+
norm_sql = redact_secrets(str(er["normalized_sql"] or ""))
|
|
134
|
+
|
|
135
|
+
if rel == "WRITES_TABLE":
|
|
136
|
+
table_writers.setdefault(tgt, [])
|
|
137
|
+
if src not in table_writers[tgt]:
|
|
138
|
+
table_writers[tgt].append(src)
|
|
139
|
+
elif rel == "READS_TABLE":
|
|
140
|
+
table_readers.setdefault(tgt, [])
|
|
141
|
+
if src not in table_readers[tgt]:
|
|
142
|
+
table_readers[tgt].append(src)
|
|
143
|
+
|
|
144
|
+
if norm_sql:
|
|
145
|
+
function_to_queries.setdefault(src, [])
|
|
146
|
+
if norm_sql not in function_to_queries[src]:
|
|
147
|
+
function_to_queries[src].append(norm_sql)
|
|
148
|
+
|
|
149
|
+
if q_trace and str(er["sample_trace_id"]) != q_trace:
|
|
150
|
+
continue
|
|
151
|
+
if (
|
|
152
|
+
q_route
|
|
153
|
+
and q_route not in src.lower()
|
|
154
|
+
and q_route not in tgt.lower()
|
|
155
|
+
and str(er["sample_trace_id"]) not in route_matched_traces
|
|
156
|
+
):
|
|
157
|
+
continue
|
|
158
|
+
if q_sym and q_sym not in src.lower() and q_sym not in tgt.lower():
|
|
159
|
+
continue
|
|
160
|
+
if q_tbl and q_tbl not in tgt.lower():
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
filtered_edges.append({
|
|
164
|
+
"edge_id": str(er["edge_id"]),
|
|
165
|
+
"source": src,
|
|
166
|
+
"target": tgt,
|
|
167
|
+
"relationship": rel,
|
|
168
|
+
"operation": op,
|
|
169
|
+
"evidence_class": "RUNTIME_OBSERVED",
|
|
170
|
+
"confidence": "HIGH",
|
|
171
|
+
"status": "RUNTIME_OBSERVED",
|
|
172
|
+
"observation_count": int(er["observation_count"]),
|
|
173
|
+
"first_seen": str(er["first_seen"]),
|
|
174
|
+
"last_seen": str(er["last_seen"]),
|
|
175
|
+
"avg_duration_ms": round(float(er["avg_duration_ms"] or 0.0), 3),
|
|
176
|
+
"max_duration_ms": round(float(er["max_duration_ms"] or 0.0), 3),
|
|
177
|
+
"sample_trace_id": str(er["sample_trace_id"]),
|
|
178
|
+
"sample_span_id": str(er["sample_span_id"]),
|
|
179
|
+
"normalized_sql": norm_sql,
|
|
180
|
+
"status_code": er["status_code"],
|
|
181
|
+
"exception_type": redact_secrets(str(er["exception_type"] or "")),
|
|
182
|
+
"runtime_generation": int(er["runtime_generation"]),
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
# Compute ROUTE <-> DATABASE from observations sharing a trace_id
|
|
186
|
+
traces_routes: dict[str, set[str]] = {}
|
|
187
|
+
traces_tables: dict[str, set[str]] = {}
|
|
188
|
+
for obs in filtered_obs or [
|
|
189
|
+
{
|
|
190
|
+
"trace_id": str(r["trace_id"]),
|
|
191
|
+
"route_path": str(r["route_path"]),
|
|
192
|
+
"http_method": str(r["http_method"]),
|
|
193
|
+
"db_table": str(r["db_table"]),
|
|
194
|
+
}
|
|
195
|
+
for r in obs_rows
|
|
196
|
+
]:
|
|
197
|
+
tid = str(obs["trace_id"])
|
|
198
|
+
rp = str(obs["route_path"])
|
|
199
|
+
hm = str(obs["http_method"] or "ANY")
|
|
200
|
+
dt = str(obs["db_table"])
|
|
201
|
+
if rp:
|
|
202
|
+
traces_routes.setdefault(tid, set()).add(f"{hm} {rp}")
|
|
203
|
+
if dt:
|
|
204
|
+
traces_tables.setdefault(tid, set()).add(dt)
|
|
205
|
+
|
|
206
|
+
for tid, rset in traces_routes.items():
|
|
207
|
+
tset = traces_tables.get(tid, set())
|
|
208
|
+
for r_item in sorted(rset):
|
|
209
|
+
for t_item in sorted(tset):
|
|
210
|
+
route_to_tables.setdefault(r_item, [])
|
|
211
|
+
if t_item not in route_to_tables[r_item]:
|
|
212
|
+
route_to_tables[r_item].append(t_item)
|
|
213
|
+
table_to_routes.setdefault(t_item, [])
|
|
214
|
+
if r_item not in table_to_routes[t_item]:
|
|
215
|
+
table_to_routes[t_item].append(r_item)
|
|
216
|
+
|
|
217
|
+
return redact_payload({
|
|
218
|
+
"status": "ok",
|
|
219
|
+
"count": len(filtered_edges),
|
|
220
|
+
"observation_count": len(filtered_obs),
|
|
221
|
+
"edge_count": len(filtered_edges),
|
|
222
|
+
"evidence_class": "RUNTIME_OBSERVED",
|
|
223
|
+
"observations": filtered_obs,
|
|
224
|
+
"edges": filtered_edges,
|
|
225
|
+
"runtime_edges": filtered_edges,
|
|
226
|
+
"reverse_maps": {
|
|
227
|
+
"table_to_writers": table_writers,
|
|
228
|
+
"table_to_readers": table_readers,
|
|
229
|
+
"route_to_database": route_to_tables,
|
|
230
|
+
"route_to_tables": route_to_tables,
|
|
231
|
+
"database_to_routes": table_to_routes,
|
|
232
|
+
"table_to_routes": table_to_routes,
|
|
233
|
+
"function_to_queries": function_to_queries,
|
|
234
|
+
},
|
|
235
|
+
"results": filtered_edges,
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def reconcile_static_runtime(
|
|
240
|
+
con: sqlite3.Connection,
|
|
241
|
+
repo_or_symbol: Any = None,
|
|
242
|
+
route: str | None = None,
|
|
243
|
+
table: str | None = None,
|
|
244
|
+
*,
|
|
245
|
+
symbol: str | None = None,
|
|
246
|
+
) -> dict[str, Any]:
|
|
247
|
+
"""Compare static code+database expectations against runtime observations (Phase 12).
|
|
248
|
+
|
|
249
|
+
Classifies each relationship into:
|
|
250
|
+
- `CONFIRMED_RUNTIME_PATH`
|
|
251
|
+
- `STATIC_RUNTIME_CONFLICT`
|
|
252
|
+
- `NOT_OBSERVED_AT_RUNTIME`
|
|
253
|
+
- `RUNTIME_ONLY_OBSERVED`
|
|
254
|
+
"""
|
|
255
|
+
if isinstance(repo_or_symbol, str) and not symbol:
|
|
256
|
+
symbol = repo_or_symbol
|
|
257
|
+
q_sym = (symbol or "").strip().lower()
|
|
258
|
+
q_route = (route or "").strip().lower()
|
|
259
|
+
q_tbl = (table or "").strip().lower().split(".")[-1]
|
|
260
|
+
|
|
261
|
+
# 1. Load static database edges from `graph_edges`
|
|
262
|
+
static_rows = con.execute(
|
|
263
|
+
"SELECT source, target, relationship, confidence, file, start_line, end_line, evidence, evidence_class "
|
|
264
|
+
"FROM graph_edges WHERE relationship IN ('READS_TABLE', 'WRITES_TABLE', 'POSSIBLE_TABLE', 'UNKNOWN_TABLE') "
|
|
265
|
+
"ORDER BY source, relationship, target"
|
|
266
|
+
).fetchall()
|
|
267
|
+
|
|
268
|
+
# 2. Load runtime database edges from `runtime_edges`
|
|
269
|
+
runtime_rows = con.execute(
|
|
270
|
+
"SELECT edge_id, source, target, relationship, operation, observation_count, "
|
|
271
|
+
"first_seen, last_seen, avg_duration_ms, sample_trace_id, normalized_sql "
|
|
272
|
+
"FROM runtime_edges WHERE relationship IN ('READS_TABLE', 'WRITES_TABLE') "
|
|
273
|
+
"ORDER BY source, relationship, target"
|
|
274
|
+
).fetchall()
|
|
275
|
+
|
|
276
|
+
def _norm_table(target_id: str) -> str:
|
|
277
|
+
return target_id.strip().lower().split(".")[-1]
|
|
278
|
+
|
|
279
|
+
# Group static edges by normalized source symbol
|
|
280
|
+
static_by_source: dict[str, list[dict[str, Any]]] = {}
|
|
281
|
+
for sr in static_rows:
|
|
282
|
+
src = str(sr["source"])
|
|
283
|
+
tgt = str(sr["target"])
|
|
284
|
+
if q_sym and q_sym not in src.lower() and q_sym != src.split(".")[-1].lower():
|
|
285
|
+
continue
|
|
286
|
+
if q_route and q_route not in src.lower():
|
|
287
|
+
continue
|
|
288
|
+
if q_tbl and q_tbl != _norm_table(tgt):
|
|
289
|
+
continue
|
|
290
|
+
static_by_source.setdefault(src, []).append({
|
|
291
|
+
"source": src,
|
|
292
|
+
"target": tgt,
|
|
293
|
+
"table_short": _norm_table(tgt),
|
|
294
|
+
"relationship": str(sr["relationship"]),
|
|
295
|
+
"confidence": str(sr["confidence"]),
|
|
296
|
+
"evidence_class": str(sr["evidence_class"]),
|
|
297
|
+
"file": str(sr["file"]),
|
|
298
|
+
"start_line": int(sr["start_line"]),
|
|
299
|
+
"evidence": redact_secrets(str(sr["evidence"])),
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
# Group runtime edges by source symbol (matching exact or short name against static symbols)
|
|
303
|
+
runtime_by_source: dict[str, list[dict[str, Any]]] = {}
|
|
304
|
+
for rr in runtime_rows:
|
|
305
|
+
r_src = str(rr["source"])
|
|
306
|
+
r_tgt = str(rr["target"])
|
|
307
|
+
if q_sym and q_sym not in r_src.lower() and q_sym != r_src.split(".")[-1].lower():
|
|
308
|
+
continue
|
|
309
|
+
if q_route and q_route not in r_src.lower():
|
|
310
|
+
continue
|
|
311
|
+
if q_tbl and q_tbl != _norm_table(r_tgt):
|
|
312
|
+
continue
|
|
313
|
+
|
|
314
|
+
# Match against static_by_source keys if short name matches
|
|
315
|
+
matched_key = r_src
|
|
316
|
+
for s_key in static_by_source:
|
|
317
|
+
if s_key.lower() == r_src.lower() or s_key.split(".")[-1].lower() == r_src.split(".")[-1].lower():
|
|
318
|
+
matched_key = s_key
|
|
319
|
+
break
|
|
320
|
+
|
|
321
|
+
runtime_by_source.setdefault(matched_key, []).append({
|
|
322
|
+
"edge_id": str(rr["edge_id"]),
|
|
323
|
+
"source": r_src,
|
|
324
|
+
"target": r_tgt,
|
|
325
|
+
"table_short": _norm_table(r_tgt),
|
|
326
|
+
"relationship": str(rr["relationship"]),
|
|
327
|
+
"operation": str(rr["operation"]),
|
|
328
|
+
"evidence_class": "RUNTIME_OBSERVED",
|
|
329
|
+
"observation_count": int(rr["observation_count"]),
|
|
330
|
+
"avg_duration_ms": round(float(rr["avg_duration_ms"] or 0.0), 3),
|
|
331
|
+
"sample_trace_id": str(rr["sample_trace_id"]),
|
|
332
|
+
"normalized_sql": redact_secrets(str(rr["normalized_sql"] or "")),
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
confirmed: list[dict[str, Any]] = []
|
|
336
|
+
conflicts: list[dict[str, Any]] = []
|
|
337
|
+
not_observed: list[dict[str, Any]] = []
|
|
338
|
+
runtime_only: list[dict[str, Any]] = []
|
|
339
|
+
all_items: list[dict[str, Any]] = []
|
|
340
|
+
|
|
341
|
+
all_sources = sorted(set(static_by_source.keys()) | set(runtime_by_source.keys()))
|
|
342
|
+
|
|
343
|
+
for src in all_sources:
|
|
344
|
+
s_edges = static_by_source.get(src, [])
|
|
345
|
+
r_edges = runtime_by_source.get(src, [])
|
|
346
|
+
|
|
347
|
+
matched_r_indices: set[int] = set()
|
|
348
|
+
|
|
349
|
+
for s_edge in s_edges:
|
|
350
|
+
s_tbl = s_edge["table_short"]
|
|
351
|
+
s_rel = s_edge["relationship"]
|
|
352
|
+
|
|
353
|
+
# 1. Exact (table, relationship) match -> CONFIRMED_RUNTIME_PATH
|
|
354
|
+
exact_idx = next(
|
|
355
|
+
(
|
|
356
|
+
i
|
|
357
|
+
for i, re_item in enumerate(r_edges)
|
|
358
|
+
if re_item["table_short"] == s_tbl and re_item["relationship"] == s_rel
|
|
359
|
+
),
|
|
360
|
+
None,
|
|
361
|
+
)
|
|
362
|
+
if exact_idx is not None:
|
|
363
|
+
matched_r_indices.add(exact_idx)
|
|
364
|
+
re_item = r_edges[exact_idx]
|
|
365
|
+
item = {
|
|
366
|
+
"reconciliation_status": ReconciliationStatus.CONFIRMED_RUNTIME_PATH.value,
|
|
367
|
+
"reconciliation_state": ReconciliationStatus.CONFIRMED_RUNTIME_PATH.value,
|
|
368
|
+
"runtime_evidence_class": "RUNTIME_OBSERVED",
|
|
369
|
+
"source": src,
|
|
370
|
+
"table": s_tbl,
|
|
371
|
+
"static": s_edge,
|
|
372
|
+
"runtime": re_item,
|
|
373
|
+
"explanation": (
|
|
374
|
+
f"Static expectation ({src} -> {s_rel} {s_tbl}) confirmed by runtime observation "
|
|
375
|
+
f"({re_item['operation']} {s_tbl}, count={re_item['observation_count']})."
|
|
376
|
+
),
|
|
377
|
+
}
|
|
378
|
+
confirmed.append(item)
|
|
379
|
+
all_items.append(item)
|
|
380
|
+
continue
|
|
381
|
+
|
|
382
|
+
# 2. If static had UNKNOWN_TABLE or POSSIBLE_TABLE and runtime observed a concrete table:
|
|
383
|
+
# Keep static as UNKNOWN/POSSIBLE and report RUNTIME_ONLY_OBSERVED without promoting static proof
|
|
384
|
+
if s_rel in ("UNKNOWN_TABLE", "POSSIBLE_TABLE") and r_edges:
|
|
385
|
+
for i, re_item in enumerate(r_edges):
|
|
386
|
+
matched_r_indices.add(i)
|
|
387
|
+
item = {
|
|
388
|
+
"reconciliation_status": ReconciliationStatus.RUNTIME_ONLY_OBSERVED.value,
|
|
389
|
+
"reconciliation_state": ReconciliationStatus.RUNTIME_ONLY_OBSERVED.value,
|
|
390
|
+
"runtime_evidence_class": "RUNTIME_OBSERVED",
|
|
391
|
+
"source": src,
|
|
392
|
+
"table": re_item["table_short"],
|
|
393
|
+
"static": s_edge,
|
|
394
|
+
"runtime": re_item,
|
|
395
|
+
"explanation": (
|
|
396
|
+
f"Static analysis was {s_rel} ({s_edge['evidence_class']}); runtime observed "
|
|
397
|
+
f"{re_item['operation']} on '{re_item['table_short']}'. Runtime observation is NOT "
|
|
398
|
+
f"converted into static AST proof."
|
|
399
|
+
),
|
|
400
|
+
}
|
|
401
|
+
runtime_only.append(item)
|
|
402
|
+
all_items.append(item)
|
|
403
|
+
continue
|
|
404
|
+
|
|
405
|
+
# 3. Static expected table A, but runtime observed this function accessing a DIFFERENT table B -> STATIC_RUNTIME_CONFLICT
|
|
406
|
+
unmatched_r = [
|
|
407
|
+
(i, re_item)
|
|
408
|
+
for i, re_item in enumerate(r_edges)
|
|
409
|
+
if re_item["table_short"] != s_tbl
|
|
410
|
+
]
|
|
411
|
+
if unmatched_r and not any(re_item["table_short"] == s_tbl for re_item in r_edges):
|
|
412
|
+
idx0, re_conflict = unmatched_r[0]
|
|
413
|
+
matched_r_indices.add(idx0)
|
|
414
|
+
item = {
|
|
415
|
+
"reconciliation_status": ReconciliationStatus.STATIC_RUNTIME_CONFLICT.value,
|
|
416
|
+
"reconciliation_state": ReconciliationStatus.STATIC_RUNTIME_CONFLICT.value,
|
|
417
|
+
"runtime_evidence_class": "RUNTIME_OBSERVED",
|
|
418
|
+
"source": src,
|
|
419
|
+
"expected_table": s_tbl,
|
|
420
|
+
"observed_table": re_conflict["table_short"],
|
|
421
|
+
"static": s_edge,
|
|
422
|
+
"runtime": re_conflict,
|
|
423
|
+
"explanation": (
|
|
424
|
+
f"Static analysis expected {src} -> {s_rel} '{s_tbl}', but runtime observed "
|
|
425
|
+
f"{re_conflict['relationship']} '{re_conflict['table_short']}'."
|
|
426
|
+
),
|
|
427
|
+
}
|
|
428
|
+
conflicts.append(item)
|
|
429
|
+
all_items.append(item)
|
|
430
|
+
continue
|
|
431
|
+
|
|
432
|
+
# 4. Otherwise, static edge was not observed in the supplied runtime telemetry
|
|
433
|
+
item = {
|
|
434
|
+
"reconciliation_status": ReconciliationStatus.NOT_OBSERVED_AT_RUNTIME.value,
|
|
435
|
+
"reconciliation_state": ReconciliationStatus.NOT_OBSERVED_AT_RUNTIME.value,
|
|
436
|
+
"runtime_evidence_class": "RUNTIME_UNOBSERVED",
|
|
437
|
+
"source": src,
|
|
438
|
+
"table": s_tbl,
|
|
439
|
+
"static": s_edge,
|
|
440
|
+
"runtime": None,
|
|
441
|
+
"explanation": (
|
|
442
|
+
f"Static relationship ({src} -> {s_rel} {s_tbl}) has no matching event in supplied "
|
|
443
|
+
f"runtime telemetry. Note: runtime telemetry is observational, not exhaustive; "
|
|
444
|
+
f"'not observed' does NOT mean 'does not happen'."
|
|
445
|
+
),
|
|
446
|
+
}
|
|
447
|
+
not_observed.append(item)
|
|
448
|
+
all_items.append(item)
|
|
449
|
+
|
|
450
|
+
# Any remaining runtime edges for this source that had no static expectation at all
|
|
451
|
+
for i, re_item in enumerate(r_edges):
|
|
452
|
+
if i not in matched_r_indices:
|
|
453
|
+
item = {
|
|
454
|
+
"reconciliation_status": ReconciliationStatus.RUNTIME_ONLY_OBSERVED.value,
|
|
455
|
+
"reconciliation_state": ReconciliationStatus.RUNTIME_ONLY_OBSERVED.value,
|
|
456
|
+
"runtime_evidence_class": "RUNTIME_OBSERVED",
|
|
457
|
+
"source": src,
|
|
458
|
+
"table": re_item["table_short"],
|
|
459
|
+
"static": None,
|
|
460
|
+
"runtime": re_item,
|
|
461
|
+
"explanation": (
|
|
462
|
+
f"Runtime observed {src} -> {re_item['relationship']} '{re_item['table_short']}' "
|
|
463
|
+
f"with no static database edge. Recorded as RUNTIME_OBSERVED without fabricating static proof."
|
|
464
|
+
),
|
|
465
|
+
}
|
|
466
|
+
runtime_only.append(item)
|
|
467
|
+
all_items.append(item)
|
|
468
|
+
|
|
469
|
+
return redact_payload({
|
|
470
|
+
"status": "ok",
|
|
471
|
+
"summary": {
|
|
472
|
+
"total_reconciled": len(all_items),
|
|
473
|
+
"confirmed_count": len(confirmed),
|
|
474
|
+
"conflict_count": len(conflicts),
|
|
475
|
+
"not_observed_count": len(not_observed),
|
|
476
|
+
"runtime_only_count": len(runtime_only),
|
|
477
|
+
},
|
|
478
|
+
"epistemic_note": (
|
|
479
|
+
"Runtime observations reflect only the supplied telemetry traces. "
|
|
480
|
+
"NOT_OBSERVED_AT_RUNTIME never implies a static path cannot execute, and "
|
|
481
|
+
"RUNTIME_ONLY_OBSERVED is never promoted to static AST_VERIFIED proof."
|
|
482
|
+
),
|
|
483
|
+
"confirmed": confirmed,
|
|
484
|
+
"confirmed_runtime_paths": confirmed,
|
|
485
|
+
"conflicts": conflicts,
|
|
486
|
+
"static_runtime_conflicts": conflicts,
|
|
487
|
+
"not_observed": not_observed,
|
|
488
|
+
"not_observed_at_runtime": not_observed,
|
|
489
|
+
"runtime_only": runtime_only,
|
|
490
|
+
"runtime_only_observed": runtime_only,
|
|
491
|
+
"results": all_items,
|
|
492
|
+
})
|
codegraph/search/__init__.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from .hybrid import (
|
|
2
2
|
SearchResult,
|
|
3
3
|
search,
|
|
4
|
+
search_code,
|
|
4
5
|
search_exact_canonical_id,
|
|
5
6
|
search_exact_qualified,
|
|
6
7
|
search_exact_symbol,
|
|
@@ -14,6 +15,7 @@ __all__ = [
|
|
|
14
15
|
"SearchResult",
|
|
15
16
|
"SemanticSearchUnavailable",
|
|
16
17
|
"search",
|
|
18
|
+
"search_code",
|
|
17
19
|
"search_exact_canonical_id",
|
|
18
20
|
"search_exact_qualified",
|
|
19
21
|
"search_exact_symbol",
|