codegraph-engine 3.0.0__2-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 (130) hide show
  1. codegraph/__init__.py +37 -0
  2. codegraph/agent.py +26 -0
  3. codegraph/agent_brain.py +2212 -0
  4. codegraph/agent_capabilities.py +4058 -0
  5. codegraph/agent_rules.py +531 -0
  6. codegraph/api_drift.py +455 -0
  7. codegraph/architecture.py +371 -0
  8. codegraph/async_queue/__init__.py +50 -0
  9. codegraph/async_queue/detectors/__init__.py +48 -0
  10. codegraph/async_queue/detectors/celery.py +160 -0
  11. codegraph/async_queue/detectors/kafka.py +168 -0
  12. codegraph/async_queue/detectors/rabbitmq.py +116 -0
  13. codegraph/async_queue/detectors/redis.py +130 -0
  14. codegraph/async_queue/detectors/sqs.py +135 -0
  15. codegraph/async_queue/models.py +161 -0
  16. codegraph/async_queue/otel_models.py +74 -0
  17. codegraph/async_queue/otel_parser.py +337 -0
  18. codegraph/async_queue/reconciler.py +427 -0
  19. codegraph/async_queue/scanner.py +291 -0
  20. codegraph/async_queue/traversal.py +447 -0
  21. codegraph/audit.py +106 -0
  22. codegraph/binding_resolver.py +616 -0
  23. codegraph/branch_comparison.py +197 -0
  24. codegraph/cache.py +95 -0
  25. codegraph/change_impact.py +547 -0
  26. codegraph/cli.py +2436 -0
  27. codegraph/cli_output.py +205 -0
  28. codegraph/config.py +45 -0
  29. codegraph/constraints.py +238 -0
  30. codegraph/context.py +2182 -0
  31. codegraph/context_freshness.py +351 -0
  32. codegraph/database/__init__.py +72 -0
  33. codegraph/database/extractor.py +3651 -0
  34. codegraph/database/interrogation.py +1494 -0
  35. codegraph/database/models.py +310 -0
  36. codegraph/database/schema_drift.py +862 -0
  37. codegraph/epistemic.py +200 -0
  38. codegraph/errors.py +282 -0
  39. codegraph/evidence/__init__.py +15 -0
  40. codegraph/evidence/citations.py +418 -0
  41. codegraph/evidence_contract.py +486 -0
  42. codegraph/frameworks.py +960 -0
  43. codegraph/frameworks_nestjs.py +502 -0
  44. codegraph/frameworks_nextjs.py +337 -0
  45. codegraph/frameworks_react.py +457 -0
  46. codegraph/freshness.py +335 -0
  47. codegraph/frontend_html.py +109 -0
  48. codegraph/git.py +362 -0
  49. codegraph/git_state.py +545 -0
  50. codegraph/graph/__init__.py +46 -0
  51. codegraph/graph/models.py +56 -0
  52. codegraph/graph/traversal.py +1993 -0
  53. codegraph/indexing/__init__.py +24 -0
  54. codegraph/indexing/classifier.py +564 -0
  55. codegraph/indexing/indexer.py +2578 -0
  56. codegraph/indexing/models.py +427 -0
  57. codegraph/indexing/parser.py +3404 -0
  58. codegraph/indexing/pool.py +136 -0
  59. codegraph/indexing/scanner.py +211 -0
  60. codegraph/indexing/telemetry.py +326 -0
  61. codegraph/indexing/templates.py +418 -0
  62. codegraph/indexing/test_framework.py +116 -0
  63. codegraph/installer.py +1702 -0
  64. codegraph/interrogation.py +2355 -0
  65. codegraph/llm/__init__.py +3 -0
  66. codegraph/llm/base.py +15 -0
  67. codegraph/llm/context.py +20 -0
  68. codegraph/mcp/__init__.py +3 -0
  69. codegraph/mcp/server.py +1476 -0
  70. codegraph/mcp_diagnostics.py +882 -0
  71. codegraph/memory/__init__.py +3 -0
  72. codegraph/memory/store.py +46 -0
  73. codegraph/models.py +310 -0
  74. codegraph/monorepo.py +925 -0
  75. codegraph/observability.py +262 -0
  76. codegraph/optimizer.py +1014 -0
  77. codegraph/planner.py +417 -0
  78. codegraph/process_lifecycle.py +1153 -0
  79. codegraph/py.typed +1 -0
  80. codegraph/query_expansion.py +199 -0
  81. codegraph/ranking.py +408 -0
  82. codegraph/refactor/__init__.py +36 -0
  83. codegraph/refactor/models.py +114 -0
  84. codegraph/refactor/renamer.py +844 -0
  85. codegraph/refactor/transactions.py +414 -0
  86. codegraph/resolver.py +3968 -0
  87. codegraph/resources/__init__.py +46 -0
  88. codegraph/resources/cache.py +117 -0
  89. codegraph/resources/coalescer.py +83 -0
  90. codegraph/resources/debouncer.py +98 -0
  91. codegraph/resources/governor.py +219 -0
  92. codegraph/resources/monitor.py +102 -0
  93. codegraph/resources/policy.py +123 -0
  94. codegraph/retrieval_policy.py +320 -0
  95. codegraph/roblox.py +2174 -0
  96. codegraph/route_composer.py +363 -0
  97. codegraph/route_topology.py +172 -0
  98. codegraph/runtime/__init__.py +51 -0
  99. codegraph/runtime/collector.py +155 -0
  100. codegraph/runtime/db_watcher.py +191 -0
  101. codegraph/runtime/ingestor.py +692 -0
  102. codegraph/runtime/models.py +130 -0
  103. codegraph/runtime/port_inspector.py +304 -0
  104. codegraph/runtime/reconciliation.py +492 -0
  105. codegraph/runtime/runner.py +353 -0
  106. codegraph/runtime/trace_linker.py +175 -0
  107. codegraph/search/__init__.py +25 -0
  108. codegraph/search/hybrid.py +846 -0
  109. codegraph/search/semantic.py +28 -0
  110. codegraph/security/__init__.py +51 -0
  111. codegraph/security/paths.py +189 -0
  112. codegraph/security/redaction.py +633 -0
  113. codegraph/semantic_conflicts.py +270 -0
  114. codegraph/semantic_decorators.py +372 -0
  115. codegraph/structural_diff.py +542 -0
  116. codegraph/styles.py +144 -0
  117. codegraph/symbol_history.py +277 -0
  118. codegraph/target_resolver.py +348 -0
  119. codegraph/task.py +637 -0
  120. codegraph/tool_selection_eval.py +1158 -0
  121. codegraph/tsconfig.py +236 -0
  122. codegraph/ui/__init__.py +6 -0
  123. codegraph/ui/server.py +755 -0
  124. codegraph/watcher.py +323 -0
  125. codegraph_engine-3.0.0.dist-info/METADATA +868 -0
  126. codegraph_engine-3.0.0.dist-info/RECORD +130 -0
  127. codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
  128. codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
  129. codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
  130. codegraph_engine-3.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,427 @@
1
+ """Static-Runtime OpenTelemetry Reconciliation Engine.
2
+
3
+ Epistemic Invariants:
4
+ 1. Do not replace static edges with runtime edges: Add runtime evidence to them.
5
+ 2. If static topic match exists, upgrade evidence to OTEL_DISTRIBUTED_VERIFIED.
6
+ 3. If consumer is external (e.g. Repo B), record as RUNTIME_ONLY_OBSERVED with service identity:
7
+ service = <c.service_name>
8
+ operation = <c.name>
9
+ repository = UNKNOWN
10
+ Do not invent a local AST symbol.
11
+ 4. Bounded aggregation: Bounded sliding reservoir of at most 100 recent durations per link.
12
+ Nearest-rank percentile estimation method with documented precision.
13
+ 5. Idempotent: Re-ingesting previously observed spans is a no-op and never duplicates counts.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import hashlib
18
+ import json
19
+ import sqlite3
20
+ import time
21
+ from typing import Any
22
+
23
+ from codegraph.async_queue.otel_models import (
24
+ OTelEvidenceStatus,
25
+ OTelSpanRecord,
26
+ TraceReconciliationResult,
27
+ )
28
+
29
+ OTEL_SCHEMA = """
30
+ CREATE TABLE IF NOT EXISTS otel_ingested_spans (
31
+ composite_id TEXT PRIMARY KEY,
32
+ trace_id TEXT NOT NULL,
33
+ span_id TEXT NOT NULL,
34
+ parent_span_id TEXT NOT NULL DEFAULT '',
35
+ service_name TEXT NOT NULL DEFAULT '',
36
+ name TEXT NOT NULL DEFAULT '',
37
+ kind TEXT NOT NULL DEFAULT '',
38
+ duration_ms REAL NOT NULL DEFAULT 0.0,
39
+ status_code TEXT NOT NULL DEFAULT 'OK',
40
+ messaging_system TEXT NOT NULL DEFAULT '',
41
+ messaging_dest TEXT NOT NULL DEFAULT '',
42
+ messaging_op TEXT NOT NULL DEFAULT '',
43
+ corr_id_hash TEXT NOT NULL DEFAULT '',
44
+ ingested_at INTEGER NOT NULL
45
+ );
46
+
47
+ CREATE INDEX IF NOT EXISTS idx_otel_spans_trace ON otel_ingested_spans(trace_id);
48
+ CREATE INDEX IF NOT EXISTS idx_otel_spans_dest ON otel_ingested_spans(messaging_dest);
49
+ CREATE INDEX IF NOT EXISTS idx_otel_spans_corr ON otel_ingested_spans(corr_id_hash);
50
+ """
51
+
52
+
53
+ def ensure_otel_tables(con: sqlite3.Connection) -> None:
54
+ """Ensure otel storage tables exist."""
55
+ con.executescript(OTEL_SCHEMA)
56
+
57
+
58
+ def compute_percentiles(samples: list[float]) -> tuple[float, float, float]:
59
+ """Compute (p50, p95, p99) using the nearest-rank method on a bounded reservoir (N <= 100)."""
60
+ if not samples:
61
+ return 0.0, 0.0, 0.0
62
+ sorted_s = sorted(samples)
63
+ n = len(sorted_s)
64
+
65
+ def _rank(p: float) -> float:
66
+ idx = max(0, min(int(round(p * n - 0.5)), n - 1))
67
+ return round(sorted_s[idx], 2)
68
+
69
+ return _rank(0.50), _rank(0.95), _rank(0.99)
70
+
71
+
72
+ class OTelTraceReconciler:
73
+ """Idempotent reconciliation engine for OpenTelemetry traces and CodeGraph async queues."""
74
+
75
+ def __init__(self, con: sqlite3.Connection) -> None:
76
+ self.con = con
77
+ self.con.row_factory = sqlite3.Row
78
+ ensure_otel_tables(self.con)
79
+
80
+ def reconcile_spans(
81
+ self,
82
+ spans: list[OTelSpanRecord],
83
+ ) -> TraceReconciliationResult:
84
+ """Idempotently ingest spans, pair publishers and consumers, and reconcile with graph."""
85
+ if not spans:
86
+ return TraceReconciliationResult(
87
+ spans_ingested=0,
88
+ spans_deduplicated=0,
89
+ links_upgraded=0,
90
+ links_created_runtime_only=0,
91
+ conflicts_detected=0,
92
+ details=[],
93
+ )
94
+
95
+ now_ts = int(time.time())
96
+
97
+ # 1. Filter out previously ingested spans (idempotence guard)
98
+ existing_keys = {
99
+ str(r[0])
100
+ for r in self.con.execute(
101
+ f"SELECT composite_id FROM otel_ingested_spans WHERE composite_id IN ({','.join(['?']*len(spans))})",
102
+ [s.composite_id for s in spans],
103
+ ).fetchall()
104
+ }
105
+
106
+ fresh_spans = [s for s in spans if s.composite_id not in existing_keys]
107
+ spans_deduplicated = len(spans) - len(fresh_spans)
108
+
109
+ if not fresh_spans:
110
+ return TraceReconciliationResult(
111
+ spans_ingested=0,
112
+ spans_deduplicated=spans_deduplicated,
113
+ links_upgraded=0,
114
+ links_created_runtime_only=0,
115
+ conflicts_detected=0,
116
+ details=[{"message": "All spans were previously ingested; zero new updates."}],
117
+ )
118
+
119
+ # 2. Insert fresh spans into otel_ingested_spans
120
+ insert_rows = [
121
+ (
122
+ s.composite_id,
123
+ s.trace_id,
124
+ s.span_id,
125
+ s.parent_span_id,
126
+ s.service_name,
127
+ s.name,
128
+ s.kind,
129
+ s.duration_ms,
130
+ s.status_code,
131
+ s.messaging_system,
132
+ s.messaging_destination,
133
+ s.messaging_operation,
134
+ s.correlation_id_hash,
135
+ now_ts,
136
+ )
137
+ for s in fresh_spans
138
+ ]
139
+ self.con.executemany(
140
+ """
141
+ INSERT OR IGNORE INTO otel_ingested_spans (
142
+ composite_id, trace_id, span_id, parent_span_id, service_name, name, kind,
143
+ duration_ms, status_code, messaging_system, messaging_dest, messaging_op,
144
+ corr_id_hash, ingested_at
145
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
146
+ """,
147
+ insert_rows,
148
+ )
149
+
150
+ # 3. Categorize into producers and consumers
151
+ producers: list[OTelSpanRecord] = []
152
+ consumers: list[OTelSpanRecord] = []
153
+
154
+ for s in fresh_spans:
155
+ if s.messaging_operation == "publish" or s.kind == "PRODUCER":
156
+ producers.append(s)
157
+ elif s.messaging_operation in ("process", "receive") or s.kind == "CONSUMER":
158
+ consumers.append(s)
159
+
160
+ # 4. Correlate producers to consumers
161
+ # Matching index:
162
+ # Key: (trace_id, destination) OR parent_span_id OR correlation_id_hash
163
+ paired_links: list[tuple[OTelSpanRecord, OTelSpanRecord, float]] = []
164
+
165
+ for p in producers:
166
+ for c in consumers:
167
+ matched = False
168
+ # Match A: Parent-child span link
169
+ if c.parent_span_id and c.parent_span_id == p.span_id:
170
+ matched = True
171
+ # Match B: Span links
172
+ elif any(l_trace == p.trace_id and l_span == p.span_id for l_trace, l_span in c.links):
173
+ matched = True
174
+ # Match C: Same trace ID + same destination
175
+ elif (
176
+ p.trace_id == c.trace_id
177
+ and p.messaging_destination
178
+ and p.messaging_destination == c.messaging_destination
179
+ ):
180
+ matched = True
181
+ # Match D: Same correlation ID hash
182
+ elif p.correlation_id_hash and p.correlation_id_hash == c.correlation_id_hash:
183
+ matched = True
184
+
185
+ if matched:
186
+ # Transit/execution latency:
187
+ if c.start_time_unix_nano > 0 and p.start_time_unix_nano > 0:
188
+ lat = max(0.0, (c.start_time_unix_nano - p.start_time_unix_nano) / 1_000_000.0)
189
+ else:
190
+ lat = max(0.0, p.duration_ms + c.duration_ms)
191
+ paired_links.append((p, c, lat))
192
+
193
+ upgraded_count = 0
194
+ runtime_only_count = 0
195
+ conflicts_count = 0
196
+ details: list[dict[str, Any]] = []
197
+
198
+ # 5. Reconcile pairs against static async_links
199
+ for p, c, lat_ms in paired_links:
200
+ dest = p.messaging_destination or c.messaging_destination
201
+ sys_name = p.messaging_system or c.messaging_system or "generic"
202
+
203
+ # Check if static link exists for destination
204
+ static_rows = self.con.execute(
205
+ "SELECT * FROM async_links WHERE destination = ?",
206
+ (dest,),
207
+ ).fetchall()
208
+
209
+ if static_rows:
210
+ # Upgrade existing static links with runtime empirical evidence
211
+ for row in static_rows:
212
+ l_id = str(row["link_id"])
213
+ meta_raw = str(row["metadata_json"] or "{}")
214
+ try:
215
+ meta = json.loads(meta_raw)
216
+ except Exception:
217
+ meta = {}
218
+
219
+ reservoir: list[float] = meta.get("durations_reservoir", [])
220
+ reservoir.append(lat_ms)
221
+ if len(reservoir) > 100:
222
+ reservoir = reservoir[-100:] # Bounded reservoir
223
+
224
+ p50, p95, p99 = compute_percentiles(reservoir)
225
+ new_trace_count = int(row["trace_count"] or 0) + 1
226
+ err_inc = 1 if (p.status_code == "ERROR" or c.status_code == "ERROR") else 0
227
+ new_error_count = int(row["error_count"] or 0) + err_inc
228
+ avg_lat = round(sum(reservoir) / len(reservoir), 2)
229
+
230
+ meta["durations_reservoir"] = reservoir
231
+ meta["last_observed_trace_id"] = p.trace_id
232
+ meta["producer_service"] = p.service_name
233
+ meta["consumer_service"] = c.service_name
234
+
235
+ self.con.execute(
236
+ """
237
+ UPDATE async_links SET
238
+ evidence_class = ?,
239
+ trace_count = ?,
240
+ avg_latency_ms = ?,
241
+ p50_ms = ?,
242
+ p95_ms = ?,
243
+ p99_ms = ?,
244
+ error_count = ?,
245
+ metadata_json = ?
246
+ WHERE link_id = ?
247
+ """,
248
+ (
249
+ OTelEvidenceStatus.OTEL_DISTRIBUTED_VERIFIED.value,
250
+ new_trace_count,
251
+ avg_lat,
252
+ p50,
253
+ p95,
254
+ p99,
255
+ new_error_count,
256
+ json.dumps(meta),
257
+ l_id,
258
+ ),
259
+ )
260
+
261
+ # Update graph_edges to reflect OTEL_DISTRIBUTED_VERIFIED
262
+ self.con.execute(
263
+ """
264
+ UPDATE graph_edges SET
265
+ evidence_class = 'OTEL_DISTRIBUTED_VERIFIED',
266
+ evidence = ?
267
+ WHERE source = ? AND target = ? AND relationship = 'STATIC_QUEUE_LINKED'
268
+ """,
269
+ (
270
+ f"Verified via OpenTelemetry trace {p.trace_id} (p50={p50}ms, p95={p95}ms)",
271
+ str(row["producer_id"]),
272
+ str(row["consumer_id"]),
273
+ ),
274
+ )
275
+ upgraded_count += 1
276
+ details.append({
277
+ "link_id": l_id,
278
+ "destination": dest,
279
+ "action": "UPGRADED_TO_OTEL_DISTRIBUTED_VERIFIED",
280
+ "p50_ms": p50,
281
+ "p95_ms": p95,
282
+ "p99_ms": p99,
283
+ "trace_count": new_trace_count,
284
+ })
285
+ else:
286
+ # No static link in repository index (e.g. cross-repository worker in Repo B)
287
+ # Invariant: Record as RUNTIME_ONLY_OBSERVED with service identity. Do NOT invent a local symbol.
288
+ seed = f"rt:{p.service_name}:{dest}:{c.service_name}:{c.name}"
289
+ link_id = hashlib.sha256(seed.encode("utf-8")).hexdigest()[:16]
290
+ q_id = f"{sys_name}:{dest}"
291
+
292
+ meta = {
293
+ "service": c.service_name or "external_worker",
294
+ "operation": c.name or "process",
295
+ "repository": "UNKNOWN",
296
+ "durations_reservoir": [lat_ms],
297
+ "producer_service": p.service_name,
298
+ "last_observed_trace_id": p.trace_id,
299
+ }
300
+ p50, p95, p99 = lat_ms, lat_ms, lat_ms
301
+
302
+ # Ensure queue exists in async_queues
303
+ self.con.execute(
304
+ """
305
+ INSERT OR IGNORE INTO async_queues (queue_id, system, destination, service)
306
+ VALUES (?, ?, ?, ?)
307
+ """,
308
+ (q_id, sys_name, dest, p.service_name),
309
+ )
310
+
311
+ # Check if this repository has a local producer for destination
312
+ prod_record = self.con.execute(
313
+ "SELECT caller_canonical_id, file_path, line FROM async_producers WHERE destination = ? LIMIT 1",
314
+ (dest,),
315
+ ).fetchone()
316
+
317
+ if prod_record:
318
+ producer_id = str(prod_record["caller_canonical_id"])
319
+ prod_file = str(prod_record["file_path"])
320
+ prod_line = int(prod_record["line"])
321
+ else:
322
+ producer_id = p.name or f"{p.service_name}:publisher"
323
+ prod_file = ""
324
+ prod_line = 0
325
+
326
+ consumer_id = f"external:{c.service_name or 'unknown'}:{c.name or 'handler'}"
327
+
328
+ self.con.execute(
329
+ """
330
+ INSERT INTO async_links (
331
+ link_id, queue_id, system, destination,
332
+ producer_id, producer_file, producer_line,
333
+ consumer_id, consumer_file, consumer_line,
334
+ evidence_class, confidence, trace_count,
335
+ avg_latency_ms, p50_ms, p95_ms, p99_ms,
336
+ error_count, metadata_json
337
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
338
+ ON CONFLICT(link_id) DO UPDATE SET
339
+ trace_count = trace_count + 1,
340
+ avg_latency_ms = excluded.avg_latency_ms,
341
+ p50_ms = excluded.p50_ms,
342
+ p95_ms = excluded.p95_ms,
343
+ p99_ms = excluded.p99_ms
344
+ """,
345
+ (
346
+ link_id,
347
+ q_id,
348
+ sys_name,
349
+ dest,
350
+ producer_id,
351
+ prod_file,
352
+ prod_line,
353
+ consumer_id,
354
+ "",
355
+ 0,
356
+ OTelEvidenceStatus.RUNTIME_ONLY_OBSERVED.value,
357
+ "HIGH",
358
+ 1,
359
+ lat_ms,
360
+ p50,
361
+ p95,
362
+ p99,
363
+ 1 if (p.status_code == "ERROR" or c.status_code == "ERROR") else 0,
364
+ json.dumps(meta),
365
+ ),
366
+ )
367
+
368
+ # If local producer file exists, insert into graph_edges; otherwise insert into runtime_edges
369
+ if prod_file:
370
+ self.con.execute(
371
+ """
372
+ INSERT OR IGNORE INTO graph_edges (
373
+ source, target, relationship, confidence, file,
374
+ start_line, end_line, evidence, evidence_class
375
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
376
+ """,
377
+ (
378
+ producer_id,
379
+ consumer_id,
380
+ "OTEL_DISTRIBUTED_VERIFIED",
381
+ "HIGH",
382
+ prod_file,
383
+ prod_line,
384
+ prod_line,
385
+ f"Cross-service runtime link via {q_id} (service: {c.service_name})",
386
+ OTelEvidenceStatus.RUNTIME_ONLY_OBSERVED.value,
387
+ ),
388
+ )
389
+ else:
390
+ edge_id = hashlib.sha256(f"{producer_id}:{consumer_id}:{q_id}".encode()).hexdigest()[:16]
391
+ self.con.execute(
392
+ """
393
+ INSERT OR IGNORE INTO runtime_edges (
394
+ edge_id, source, target, relationship, operation, evidence_class, observation_count
395
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
396
+ """,
397
+ (
398
+ edge_id,
399
+ producer_id,
400
+ consumer_id,
401
+ "OTEL_DISTRIBUTED_VERIFIED",
402
+ dest,
403
+ OTelEvidenceStatus.RUNTIME_ONLY_OBSERVED.value,
404
+ 1,
405
+ ),
406
+ )
407
+ runtime_only_count += 1
408
+ details.append({
409
+ "link_id": link_id,
410
+ "destination": dest,
411
+ "action": "CREATED_RUNTIME_ONLY_OBSERVED",
412
+ "consumer_service": c.service_name,
413
+ "consumer_operation": c.name,
414
+ "repository": "UNKNOWN",
415
+ "latency_ms": lat_ms,
416
+ })
417
+
418
+ self.con.commit()
419
+
420
+ return TraceReconciliationResult(
421
+ spans_ingested=len(fresh_spans),
422
+ spans_deduplicated=spans_deduplicated,
423
+ links_upgraded=upgraded_count,
424
+ links_created_runtime_only=runtime_only_count,
425
+ conflicts_detected=conflicts_count,
426
+ details=details,
427
+ )