cseq 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.
cseq/event_store.py ADDED
@@ -0,0 +1,378 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict
4
+ import json
5
+ import sqlite3
6
+ from pathlib import Path
7
+ from typing import Iterable
8
+
9
+ from .runtime import RuntimeEvent, TraceSession
10
+
11
+ SCHEMA_VERSION = 4
12
+
13
+
14
+ class EventStore:
15
+ """Small SQLite-backed runtime event store.
16
+
17
+ This is the first persistent Event Store layer. It preserves ingest order and
18
+ indexes marker/cpu/task for later viewport/query work. It does not infer a
19
+ cross-CPU total order.
20
+ """
21
+
22
+ def __init__(self, path: str | Path):
23
+ self.path = Path(path)
24
+ self.path.parent.mkdir(parents=True, exist_ok=True)
25
+ self.db = sqlite3.connect(self.path)
26
+ self.db.row_factory = sqlite3.Row
27
+ self._init_schema()
28
+
29
+ def _init_schema(self) -> None:
30
+ self.db.executescript(
31
+ """
32
+ CREATE TABLE IF NOT EXISTS meta(key TEXT PRIMARY KEY, value TEXT NOT NULL);
33
+ CREATE TABLE IF NOT EXISTS events(
34
+ session TEXT NOT NULL,
35
+ event_index INTEGER NOT NULL,
36
+ marker_id TEXT NOT NULL,
37
+ raw_line TEXT NOT NULL,
38
+ timestamp_raw TEXT,
39
+ cpu_hint TEXT,
40
+ task_hint TEXT,
41
+ clock_domain TEXT,
42
+ message TEXT,
43
+ payload_json TEXT NOT NULL,
44
+ PRIMARY KEY(session, event_index)
45
+ );
46
+ CREATE TABLE IF NOT EXISTS sessions(
47
+ session TEXT PRIMARY KEY,
48
+ status TEXT NOT NULL,
49
+ event_count INTEGER NOT NULL DEFAULT 0
50
+ );
51
+ CREATE TABLE IF NOT EXISTS event_chunks(
52
+ session TEXT NOT NULL,
53
+ chunk_index INTEGER NOT NULL,
54
+ start_event_index INTEGER NOT NULL,
55
+ end_event_index INTEGER NOT NULL,
56
+ min_timestamp_raw TEXT,
57
+ max_timestamp_raw TEXT,
58
+ cpu_json TEXT NOT NULL,
59
+ task_json TEXT NOT NULL,
60
+ PRIMARY KEY(session, chunk_index)
61
+ );
62
+ CREATE INDEX IF NOT EXISTS idx_chunks_range ON event_chunks(session,start_event_index,end_event_index);
63
+ """
64
+ )
65
+ self.db.execute("INSERT OR REPLACE INTO meta(key,value) VALUES('schema_version',?)", (str(SCHEMA_VERSION),))
66
+ importing = self.db.execute("SELECT COUNT(*) FROM sessions WHERE status='IMPORTING'").fetchone()[0]
67
+ if not importing:
68
+ self._create_secondary_indexes()
69
+ self.db.commit()
70
+
71
+ def _drop_secondary_indexes(self) -> None:
72
+ self.db.executescript(
73
+ """
74
+ DROP INDEX IF EXISTS idx_events_marker;
75
+ DROP INDEX IF EXISTS idx_events_cpu;
76
+ DROP INDEX IF EXISTS idx_events_task;
77
+ """
78
+ )
79
+
80
+ def _create_secondary_indexes(self) -> None:
81
+ self.db.executescript(
82
+ """
83
+ CREATE INDEX IF NOT EXISTS idx_events_marker ON events(session, marker_id, event_index);
84
+ CREATE INDEX IF NOT EXISTS idx_events_cpu ON events(session, cpu_hint, event_index);
85
+ CREATE INDEX IF NOT EXISTS idx_events_task ON events(session, task_hint, event_index);
86
+ """
87
+ )
88
+
89
+ def close(self) -> None:
90
+ self.db.close()
91
+
92
+ def __enter__(self) -> "EventStore":
93
+ return self
94
+
95
+ def __exit__(self, *_exc) -> None:
96
+ self.close()
97
+
98
+ def replace_session(self, session: TraceSession, *, chunk_size: int = 1000, batch_size: int = 10000) -> int:
99
+ return self.replace_session_events(
100
+ session.name, session.events, chunk_size=chunk_size, batch_size=batch_size
101
+ )
102
+
103
+ def replace_session_events(
104
+ self,
105
+ session: str,
106
+ events: Iterable[RuntimeEvent],
107
+ *,
108
+ chunk_size: int = 1000,
109
+ batch_size: int = 10000,
110
+ bulk_rebuild_indexes: bool = False,
111
+ ) -> int:
112
+ """Replace one session from an iterable without materializing all events.
113
+
114
+ This is the large-runtime ingest path. Rows are inserted in bounded batches
115
+ while chunk metadata is accumulated independently. The caller may pass a
116
+ generator for multi-million-event imports.
117
+ """
118
+ chunk_size = max(1, int(chunk_size))
119
+ batch_size = max(1, int(batch_size))
120
+ count = 0
121
+ batch: list[tuple[object, ...]] = []
122
+ chunk_index = 0
123
+ chunk_count = 0
124
+ chunk_start_index: int | None = None
125
+ chunk_end_index: int | None = None
126
+ chunk_min_time: str | None = None
127
+ chunk_max_time: str | None = None
128
+ chunk_cpus: set[str] = set()
129
+ chunk_tasks: set[str] = set()
130
+
131
+ def flush_batch() -> None:
132
+ nonlocal batch
133
+ if not batch:
134
+ return
135
+ self.db.executemany(
136
+ """INSERT INTO events(session,event_index,marker_id,raw_line,timestamp_raw,cpu_hint,task_hint,clock_domain,message,payload_json)
137
+ VALUES(?,?,?,?,?,?,?,?,?,?)""",
138
+ batch,
139
+ )
140
+ batch = []
141
+
142
+ def flush_chunk() -> None:
143
+ nonlocal chunk_index, chunk_count, chunk_start_index, chunk_end_index
144
+ nonlocal chunk_min_time, chunk_max_time, chunk_cpus, chunk_tasks
145
+ if chunk_count == 0 or chunk_start_index is None or chunk_end_index is None:
146
+ return
147
+ self.db.execute(
148
+ """INSERT INTO event_chunks(session,chunk_index,start_event_index,end_event_index,min_timestamp_raw,max_timestamp_raw,cpu_json,task_json)
149
+ VALUES(?,?,?,?,?,?,?,?)""",
150
+ (
151
+ session, chunk_index, chunk_start_index, chunk_end_index,
152
+ chunk_min_time, chunk_max_time,
153
+ json.dumps(sorted(chunk_cpus)), json.dumps(sorted(chunk_tasks)),
154
+ ),
155
+ )
156
+ chunk_index += 1
157
+ chunk_count = 0
158
+ chunk_start_index = None
159
+ chunk_end_index = None
160
+ chunk_min_time = None
161
+ chunk_max_time = None
162
+ chunk_cpus = set()
163
+ chunk_tasks = set()
164
+
165
+ if bulk_rebuild_indexes:
166
+ self._drop_secondary_indexes()
167
+ try:
168
+ with self.db:
169
+ self.db.execute("DELETE FROM events WHERE session=?", (session,))
170
+ self.db.execute("DELETE FROM event_chunks WHERE session=?", (session,))
171
+ for e in events:
172
+ batch.append((
173
+ session, e.event_index, e.marker_id, e.raw_line, e.timestamp_raw,
174
+ e.cpu_hint, e.task_hint, e.clock_domain, e.message,
175
+ json.dumps(e.payload_dict(), ensure_ascii=False, sort_keys=True),
176
+ ))
177
+ if chunk_count == 0:
178
+ chunk_start_index = e.event_index
179
+ chunk_end_index = e.event_index
180
+ chunk_count += 1
181
+ if e.timestamp_raw is not None:
182
+ chunk_min_time = e.timestamp_raw if chunk_min_time is None else min(chunk_min_time, e.timestamp_raw)
183
+ chunk_max_time = e.timestamp_raw if chunk_max_time is None else max(chunk_max_time, e.timestamp_raw)
184
+ if e.cpu_hint:
185
+ chunk_cpus.add(e.cpu_hint)
186
+ if e.task_hint:
187
+ chunk_tasks.add(e.task_hint)
188
+ count += 1
189
+ if len(batch) >= batch_size:
190
+ flush_batch()
191
+ if chunk_count >= chunk_size:
192
+ flush_chunk()
193
+ flush_batch()
194
+ flush_chunk()
195
+ finally:
196
+ if bulk_rebuild_indexes:
197
+ self._create_secondary_indexes()
198
+ self.db.commit()
199
+ return count
200
+
201
+ def replace_session_events_chunked(
202
+ self,
203
+ session: str,
204
+ events: Iterable[RuntimeEvent],
205
+ *,
206
+ chunk_size: int = 10000,
207
+ batch_size: int = 50000,
208
+ commit_every: int = 250000,
209
+ rebuild_indexes: bool = True,
210
+ ) -> int:
211
+ """Import a very large session using bounded transactions.
212
+
213
+ Partial data is marked IMPORTING and only becomes READY after the full
214
+ import (and optional secondary-index rebuild) completes.
215
+ """
216
+ chunk_size=max(1,int(chunk_size)); batch_size=max(1,int(batch_size)); commit_every=max(batch_size,int(commit_every))
217
+ if rebuild_indexes:
218
+ self._drop_secondary_indexes()
219
+ self.db.execute("INSERT OR REPLACE INTO sessions(session,status,event_count) VALUES(?, 'IMPORTING', 0)",(session,))
220
+ self.db.execute("DELETE FROM events WHERE session=?",(session,))
221
+ self.db.execute("DELETE FROM event_chunks WHERE session=?",(session,))
222
+ self.db.commit()
223
+ count=0; since_commit=0; batch=[]
224
+ chunk_index=0; chunk_count=0; chunk_start=None; chunk_end=None; min_time=None; max_time=None; cpus=set(); tasks=set()
225
+ def flush_batch():
226
+ nonlocal batch
227
+ if batch:
228
+ self.db.executemany("INSERT INTO events(session,event_index,marker_id,raw_line,timestamp_raw,cpu_hint,task_hint,clock_domain,message,payload_json) VALUES(?,?,?,?,?,?,?,?,?,?)",batch); batch=[]
229
+ def flush_chunk():
230
+ nonlocal chunk_index,chunk_count,chunk_start,chunk_end,min_time,max_time,cpus,tasks
231
+ if not chunk_count:return
232
+ self.db.execute("INSERT INTO event_chunks(session,chunk_index,start_event_index,end_event_index,min_timestamp_raw,max_timestamp_raw,cpu_json,task_json) VALUES(?,?,?,?,?,?,?,?)",(session,chunk_index,chunk_start,chunk_end,min_time,max_time,json.dumps(sorted(cpus)),json.dumps(sorted(tasks))))
233
+ chunk_index+=1; chunk_count=0; chunk_start=None; chunk_end=None; min_time=None; max_time=None; cpus=set(); tasks=set()
234
+ try:
235
+ for e in events:
236
+ batch.append((session,e.event_index,e.marker_id,e.raw_line,e.timestamp_raw,e.cpu_hint,e.task_hint,e.clock_domain,e.message,json.dumps(e.payload_dict(),ensure_ascii=False,sort_keys=True)))
237
+ if chunk_count==0: chunk_start=e.event_index
238
+ chunk_end=e.event_index; chunk_count+=1; count+=1; since_commit+=1
239
+ if e.timestamp_raw is not None:
240
+ min_time=e.timestamp_raw if min_time is None else min(min_time,e.timestamp_raw); max_time=e.timestamp_raw if max_time is None else max(max_time,e.timestamp_raw)
241
+ if e.cpu_hint: cpus.add(e.cpu_hint)
242
+ if e.task_hint: tasks.add(e.task_hint)
243
+ if len(batch)>=batch_size: flush_batch()
244
+ if chunk_count>=chunk_size: flush_chunk()
245
+ if since_commit>=commit_every:
246
+ flush_batch(); self.db.execute("UPDATE sessions SET event_count=? WHERE session=?",(count,session)); self.db.commit(); since_commit=0
247
+ flush_batch(); flush_chunk(); self.db.execute("UPDATE sessions SET event_count=? WHERE session=?",(count,session)); self.db.commit()
248
+ if rebuild_indexes:
249
+ self._create_secondary_indexes(); self.db.commit()
250
+ self.db.execute("UPDATE sessions SET status='READY', event_count=? WHERE session=?",(count,session)); self.db.commit()
251
+ return count
252
+ except BaseException:
253
+ try:
254
+ flush_batch(); self.db.execute("UPDATE sessions SET status='IMPORTING', event_count=? WHERE session=?",(count,session)); self.db.commit()
255
+ finally:
256
+ pass
257
+ raise
258
+
259
+ def resume_session_events_chunked(
260
+ self,
261
+ session: str,
262
+ events: Iterable[RuntimeEvent],
263
+ *,
264
+ chunk_size: int = 10000,
265
+ batch_size: int = 50000,
266
+ commit_every: int = 250000,
267
+ finalize: bool = False,
268
+ ) -> int:
269
+ """Append to an IMPORTING session and optionally finalize it."""
270
+ row=self.db.execute("SELECT status,event_count FROM sessions WHERE session=?",(session,)).fetchone()
271
+ if row is None or row[0] != 'IMPORTING':
272
+ raise ValueError(f"session {session!r} is not IMPORTING")
273
+ existing=int(row[1])
274
+ chunk_size=max(1,int(chunk_size)); batch_size=max(1,int(batch_size)); commit_every=max(batch_size,int(commit_every))
275
+ chunk_row=self.db.execute("SELECT COALESCE(MAX(chunk_index),-1) FROM event_chunks WHERE session=?",(session,)).fetchone()
276
+ chunk_index=int(chunk_row[0])+1
277
+ count=existing; added=0; since_commit=0; batch=[]
278
+ chunk_count=0; chunk_start=None; chunk_end=None; min_time=None; max_time=None; cpus=set(); tasks=set()
279
+ def flush_batch():
280
+ nonlocal batch
281
+ if batch:
282
+ self.db.executemany("INSERT INTO events(session,event_index,marker_id,raw_line,timestamp_raw,cpu_hint,task_hint,clock_domain,message,payload_json) VALUES(?,?,?,?,?,?,?,?,?,?)",batch); batch=[]
283
+ def flush_chunk():
284
+ nonlocal chunk_index,chunk_count,chunk_start,chunk_end,min_time,max_time,cpus,tasks
285
+ if not chunk_count:return
286
+ self.db.execute("INSERT INTO event_chunks(session,chunk_index,start_event_index,end_event_index,min_timestamp_raw,max_timestamp_raw,cpu_json,task_json) VALUES(?,?,?,?,?,?,?,?)",(session,chunk_index,chunk_start,chunk_end,min_time,max_time,json.dumps(sorted(cpus)),json.dumps(sorted(tasks))))
287
+ chunk_index+=1; chunk_count=0; chunk_start=None; chunk_end=None; min_time=None; max_time=None; cpus=set(); tasks=set()
288
+ for e in events:
289
+ batch.append((session,e.event_index,e.marker_id,e.raw_line,e.timestamp_raw,e.cpu_hint,e.task_hint,e.clock_domain,e.message,json.dumps(e.payload_dict(),ensure_ascii=False,sort_keys=True)))
290
+ if chunk_count==0: chunk_start=e.event_index
291
+ chunk_end=e.event_index; chunk_count+=1; added+=1; count+=1; since_commit+=1
292
+ if e.timestamp_raw is not None:
293
+ min_time=e.timestamp_raw if min_time is None else min(min_time,e.timestamp_raw); max_time=e.timestamp_raw if max_time is None else max(max_time,e.timestamp_raw)
294
+ if e.cpu_hint: cpus.add(e.cpu_hint)
295
+ if e.task_hint: tasks.add(e.task_hint)
296
+ if len(batch)>=batch_size: flush_batch()
297
+ if chunk_count>=chunk_size: flush_chunk()
298
+ if since_commit>=commit_every:
299
+ flush_batch(); self.db.execute("UPDATE sessions SET event_count=? WHERE session=?",(count,session)); self.db.commit(); since_commit=0
300
+ flush_batch(); flush_chunk(); self.db.execute("UPDATE sessions SET event_count=? WHERE session=?",(count,session)); self.db.commit()
301
+ if finalize:
302
+ self._create_secondary_indexes(); self.db.commit()
303
+ self.db.execute("UPDATE sessions SET status='READY' WHERE session=?",(session,)); self.db.commit()
304
+ return added
305
+
306
+ def session_status(self, session: str) -> str | None:
307
+ row=self.db.execute("SELECT status FROM sessions WHERE session=?",(session,)).fetchone()
308
+ return None if row is None else str(row[0])
309
+
310
+ def count(self, session: str) -> int:
311
+ return int(self.db.execute("SELECT COUNT(*) FROM events WHERE session=?", (session,)).fetchone()[0])
312
+
313
+ def fetch_window(self, session: str, *, offset: int = 0, limit: int = 500) -> list[RuntimeEvent]:
314
+ rows = self.db.execute(
315
+ "SELECT * FROM events WHERE session=? ORDER BY event_index LIMIT ? OFFSET ?",
316
+ (session, int(limit), int(offset)),
317
+ ).fetchall()
318
+ return [self._row_to_event(r) for r in rows]
319
+
320
+ def by_marker(self, session: str, marker_id: str, *, limit: int = 500) -> list[RuntimeEvent]:
321
+ rows = self.db.execute(
322
+ "SELECT * FROM events WHERE session=? AND marker_id=? ORDER BY event_index LIMIT ?",
323
+ (session, marker_id, int(limit)),
324
+ ).fetchall()
325
+ return [self._row_to_event(r) for r in rows]
326
+
327
+ def summary(self, session: str) -> dict[str, object]:
328
+ count = self.count(session)
329
+ cpus = [r[0] for r in self.db.execute("SELECT DISTINCT cpu_hint FROM events WHERE session=? AND cpu_hint IS NOT NULL ORDER BY cpu_hint", (session,))]
330
+ tasks = [r[0] for r in self.db.execute("SELECT DISTINCT task_hint FROM events WHERE session=? AND task_hint IS NOT NULL ORDER BY task_hint", (session,))]
331
+ return {"session": session, "count": count, "cpus": cpus, "tasks": tasks, "schema_version": SCHEMA_VERSION}
332
+
333
+
334
+ def chunk_summary(self, session: str) -> list[dict[str, object]]:
335
+ rows = self.db.execute(
336
+ "SELECT * FROM event_chunks WHERE session=? ORDER BY chunk_index", (session,)
337
+ ).fetchall()
338
+ return [
339
+ {
340
+ "chunk_index": r["chunk_index"],
341
+ "start_event_index": r["start_event_index"],
342
+ "end_event_index": r["end_event_index"],
343
+ "min_timestamp_raw": r["min_timestamp_raw"],
344
+ "max_timestamp_raw": r["max_timestamp_raw"],
345
+ "cpus": json.loads(r["cpu_json"]),
346
+ "tasks": json.loads(r["task_json"]),
347
+ }
348
+ for r in rows
349
+ ]
350
+
351
+ def fetch_event_index_range(self, session: str, start_index: int, end_index: int) -> list[RuntimeEvent]:
352
+ rows = self.db.execute(
353
+ "SELECT * FROM events WHERE session=? AND event_index BETWEEN ? AND ? ORDER BY event_index",
354
+ (session, int(start_index), int(end_index)),
355
+ ).fetchall()
356
+ return [self._row_to_event(r) for r in rows]
357
+
358
+ def iter_events(self, session: str, *, batch_size: int = 5000):
359
+ """Stream events in ingest order without OFFSET-based pagination."""
360
+ cur = self.db.execute(
361
+ "SELECT * FROM events WHERE session=? ORDER BY event_index", (session,)
362
+ )
363
+ size = max(1, int(batch_size))
364
+ while True:
365
+ rows = cur.fetchmany(size)
366
+ if not rows:
367
+ break
368
+ for row in rows:
369
+ yield self._row_to_event(row)
370
+
371
+ @staticmethod
372
+ def _row_to_event(r: sqlite3.Row) -> RuntimeEvent:
373
+ payload = tuple(sorted(json.loads(r["payload_json"]).items()))
374
+ return RuntimeEvent(
375
+ event_index=r["event_index"], marker_id=r["marker_id"], raw_line=r["raw_line"],
376
+ timestamp_raw=r["timestamp_raw"], cpu_hint=r["cpu_hint"], task_hint=r["task_hint"],
377
+ clock_domain=r["clock_domain"], message=r["message"], payload=payload,
378
+ )
cseq/explain.py ADDED
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict
4
+
5
+ from .model import ProjectIndex
6
+
7
+
8
+ def explain_call(index: ProjectIndex, *, caller: str, callee_text: str | None = None) -> dict:
9
+ matches = [c for c in index.callsites if c.caller_name == caller]
10
+ if callee_text is not None:
11
+ matches = [c for c in matches if c.callee_name == callee_text or callee_text in c.raw_text]
12
+ if not matches:
13
+ return {"found": False, "caller": caller, "query": callee_text, "calls": []}
14
+
15
+ functions = {f.qualified_id: f for f in index.functions()}
16
+ flow_by_target: dict[str, list] = {}
17
+ for edge in index.flow_edges:
18
+ flow_by_target.setdefault(edge.target_key, []).append(edge)
19
+
20
+ calls = []
21
+ for call in matches:
22
+ targets = []
23
+ for tid in call.target_function_ids:
24
+ fn = functions.get(tid)
25
+ targets.append({
26
+ "function_id": tid,
27
+ "name": fn.name if fn else None,
28
+ "source": fn.source_path if fn else None,
29
+ })
30
+ if call.target_function_id and call.target_function_id not in call.target_function_ids:
31
+ fn = functions.get(call.target_function_id)
32
+ targets.append({
33
+ "function_id": call.target_function_id,
34
+ "name": fn.name if fn else None,
35
+ "source": fn.source_path if fn else None,
36
+ })
37
+
38
+ flow = []
39
+ if call.callee_slot_key:
40
+ queue = [call.callee_slot_key]
41
+ seen = set()
42
+ while queue and len(flow) < 64:
43
+ target = queue.pop(0)
44
+ if target in seen:
45
+ continue
46
+ seen.add(target)
47
+ for edge in flow_by_target.get(target, []):
48
+ flow.append({
49
+ "kind": edge.kind.value,
50
+ "from": edge.source_key,
51
+ "to": edge.target_key,
52
+ "source": edge.source_path,
53
+ })
54
+ queue.append(edge.source_key)
55
+
56
+ cpu = []
57
+ target_ids = set(call.target_function_ids)
58
+ if call.target_function_id:
59
+ target_ids.add(call.target_function_id)
60
+ for ev in index.cpu_evidence:
61
+ if ev.target_function_id in target_ids:
62
+ cpu.append({
63
+ "function_id": ev.target_function_id,
64
+ "layer": ev.layer.value,
65
+ "kind": ev.value_kind.value,
66
+ "value": ev.value,
67
+ "provenance": ev.provenance,
68
+ "explicit_override": ev.explicit_override,
69
+ })
70
+
71
+ linker = []
72
+ for ev in index.linker_evidence:
73
+ if ev.symbol_name == (call.callee_name or "") or target_ids.intersection(ev.matched_function_ids):
74
+ linker.append({
75
+ "symbol": ev.symbol_name, "address": ev.address,
76
+ "object_file": ev.object_file, "section": ev.section,
77
+ "matched_function_ids": list(ev.matched_function_ids),
78
+ "provenance": ev.provenance,
79
+ })
80
+
81
+ binary = []
82
+ for ev in index.binary_evidence:
83
+ if ev.symbol_name == (call.callee_name or "") or target_ids.intersection(ev.matched_function_ids):
84
+ binary.append({
85
+ "symbol": ev.symbol_name, "address": ev.address, "size": ev.size,
86
+ "kind": ev.symbol_kind, "matched_function_ids": list(ev.matched_function_ids),
87
+ "binary_hash": ev.binary_hash, "provenance": ev.provenance,
88
+ })
89
+
90
+ dwarf = []
91
+ for ev in index.dwarf_evidence:
92
+ if ev.function_id in target_ids:
93
+ dwarf.append({
94
+ "function_id": ev.function_id, "source": ev.source_path,
95
+ "line": ev.line, "address": ev.address,
96
+ "dwarf_file": ev.dwarf_file, "provenance": ev.provenance,
97
+ })
98
+
99
+ calls.append({
100
+ "source": call.source_path,
101
+ "line": call.source_range.start_line,
102
+ "raw": call.raw_text,
103
+ "resolution": call.target_kind.value,
104
+ "unknown_possible": call.unknown_possible,
105
+ "callee_slot": call.callee_slot_key,
106
+ "targets": targets,
107
+ "value_flow": flow,
108
+ "cpu_evidence": cpu,
109
+ "linker_evidence": linker,
110
+ "binary_evidence": binary,
111
+ "dwarf_evidence": dwarf,
112
+ })
113
+ return {"found": True, "caller": caller, "query": callee_text, "calls": calls}