codegraph-engine 2.3.0__py3-none-any.whl → 3.0.0__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 +172 -2
- codegraph/agent_capabilities.py +320 -3
- codegraph/agent_rules.py +1 -1
- codegraph/api_drift.py +455 -0
- codegraph/async_queue/__init__.py +50 -0
- codegraph/async_queue/detectors/__init__.py +48 -0
- codegraph/async_queue/detectors/celery.py +160 -0
- codegraph/async_queue/detectors/kafka.py +168 -0
- codegraph/async_queue/detectors/rabbitmq.py +116 -0
- codegraph/async_queue/detectors/redis.py +130 -0
- codegraph/async_queue/detectors/sqs.py +135 -0
- codegraph/async_queue/models.py +161 -0
- codegraph/async_queue/otel_models.py +74 -0
- codegraph/async_queue/otel_parser.py +337 -0
- codegraph/async_queue/reconciler.py +427 -0
- codegraph/async_queue/scanner.py +291 -0
- codegraph/async_queue/traversal.py +447 -0
- codegraph/change_impact.py +320 -110
- codegraph/cli.py +776 -25
- codegraph/cli_output.py +17 -2
- codegraph/context.py +38 -11
- codegraph/context_freshness.py +2 -0
- codegraph/database/__init__.py +14 -0
- codegraph/database/extractor.py +712 -17
- codegraph/database/interrogation.py +388 -2
- codegraph/database/schema_drift.py +862 -0
- codegraph/epistemic.py +7 -0
- codegraph/evidence_contract.py +21 -0
- codegraph/frameworks.py +171 -6
- codegraph/frameworks_nestjs.py +502 -0
- codegraph/frameworks_nextjs.py +337 -0
- codegraph/frameworks_react.py +457 -0
- codegraph/freshness.py +2 -0
- codegraph/frontend_html.py +109 -0
- codegraph/git.py +2 -0
- codegraph/git_state.py +2 -0
- codegraph/indexing/__init__.py +7 -1
- codegraph/indexing/indexer.py +291 -38
- codegraph/indexing/models.py +6 -0
- codegraph/indexing/parser.py +239 -0
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/scanner.py +6 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/interrogation.py +128 -20
- codegraph/mcp/server.py +190 -28
- codegraph/models.py +7 -0
- codegraph/monorepo.py +3 -0
- codegraph/optimizer.py +42 -5
- codegraph/process_lifecycle.py +31 -13
- codegraph/refactor/__init__.py +36 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +403 -0
- codegraph/resolver.py +1202 -159
- codegraph/retrieval_policy.py +6 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +19 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +30 -10
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/runner.py +155 -6
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/search/hybrid.py +26 -0
- codegraph/semantic_decorators.py +34 -3
- codegraph/structural_diff.py +2 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +2 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +715 -0
- codegraph/watcher.py +323 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.0.dist-info}/METADATA +150 -38
- codegraph_engine-3.0.0.dist-info/RECORD +129 -0
- codegraph_engine-2.3.0.dist-info/RECORD +0 -94
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.0.dist-info}/WHEEL +0 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.0.dist-info}/entry_points.txt +0 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""AWS SQS message producer and consumer detector.
|
|
2
|
+
|
|
3
|
+
Detects:
|
|
4
|
+
- Publishers / Producers: sqs.send_message(QueueUrl=...), sqs.send_message_batch()
|
|
5
|
+
- Consumers / Workers: sqs.receive_message(QueueUrl=...), @sqs_handler()
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from codegraph.async_queue.models import (
|
|
13
|
+
AsyncConsumerRecord,
|
|
14
|
+
AsyncMessagingSystem,
|
|
15
|
+
AsyncProducerRecord,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _extract_queue_name(url_or_name: str) -> str:
|
|
20
|
+
"""Extract queue name from SQS URL or name string."""
|
|
21
|
+
clean = url_or_name.strip().rstrip("/")
|
|
22
|
+
if "/" in clean:
|
|
23
|
+
return clean.split("/")[-1]
|
|
24
|
+
return clean
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def detect_sqs(
|
|
28
|
+
tree: ast.AST,
|
|
29
|
+
content: str,
|
|
30
|
+
file_path: str,
|
|
31
|
+
module_name: str,
|
|
32
|
+
) -> tuple[list[AsyncProducerRecord], list[AsyncConsumerRecord]]:
|
|
33
|
+
producers: list[AsyncProducerRecord] = []
|
|
34
|
+
consumers: list[AsyncConsumerRecord] = []
|
|
35
|
+
lines = content.splitlines()
|
|
36
|
+
|
|
37
|
+
def get_line_snippet(lineno: int) -> str:
|
|
38
|
+
if 1 <= lineno <= len(lines):
|
|
39
|
+
return lines[lineno - 1].strip()
|
|
40
|
+
return ""
|
|
41
|
+
|
|
42
|
+
class SQSVisitor(ast.NodeVisitor):
|
|
43
|
+
def __init__(self) -> None:
|
|
44
|
+
self.current_function: str = f"{module_name}.<module>"
|
|
45
|
+
|
|
46
|
+
def visit_FunctionDef(self, fn: ast.FunctionDef) -> None:
|
|
47
|
+
prev = self.current_function
|
|
48
|
+
self.current_function = f"{module_name}.{fn.name}"
|
|
49
|
+
# Check for decorator like @sqs_handler
|
|
50
|
+
for dec in fn.decorator_list:
|
|
51
|
+
dec_func = dec.func if isinstance(dec, ast.Call) else dec
|
|
52
|
+
dec_name = getattr(dec_func, "id", "") or getattr(dec_func, "attr", "")
|
|
53
|
+
if "sqs" in dec_name.lower():
|
|
54
|
+
q_name = ""
|
|
55
|
+
if isinstance(dec, ast.Call):
|
|
56
|
+
for kw in dec.keywords:
|
|
57
|
+
if kw.arg in ("queue", "queue_name", "queue_url") and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
|
|
58
|
+
q_name = _extract_queue_name(kw.value.value)
|
|
59
|
+
if q_name:
|
|
60
|
+
consumers.append(
|
|
61
|
+
AsyncConsumerRecord(
|
|
62
|
+
system=AsyncMessagingSystem.AWS_SQS,
|
|
63
|
+
destination=q_name,
|
|
64
|
+
queue_id=f"aws_sqs:{q_name}",
|
|
65
|
+
handler_canonical_id=f"{module_name}.{fn.name}",
|
|
66
|
+
file_path=file_path,
|
|
67
|
+
line=fn.lineno,
|
|
68
|
+
handler_snippet=get_line_snippet(fn.lineno),
|
|
69
|
+
confidence="HIGH",
|
|
70
|
+
metadata={"decorator": dec_name},
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
self.generic_visit(fn)
|
|
74
|
+
self.current_function = prev
|
|
75
|
+
|
|
76
|
+
def visit_AsyncFunctionDef(self, fn: ast.AsyncFunctionDef) -> None:
|
|
77
|
+
prev = self.current_function
|
|
78
|
+
self.current_function = f"{module_name}.{fn.name}"
|
|
79
|
+
self.generic_visit(fn)
|
|
80
|
+
self.current_function = prev
|
|
81
|
+
|
|
82
|
+
def visit_Call(self, node: ast.Call) -> None:
|
|
83
|
+
if isinstance(node.func, ast.Attribute):
|
|
84
|
+
attr = node.func.attr
|
|
85
|
+
# Producer: send_message, send_message_batch
|
|
86
|
+
if attr in ("send_message", "send_message_batch"):
|
|
87
|
+
q_name = ""
|
|
88
|
+
meta: dict[str, Any] = {"method": attr}
|
|
89
|
+
for kw in node.keywords:
|
|
90
|
+
if kw.arg == "QueueUrl" and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
|
|
91
|
+
q_name = _extract_queue_name(kw.value.value)
|
|
92
|
+
elif kw.arg in ("DelaySeconds", "MessageDeduplicationId", "MessageGroupId"):
|
|
93
|
+
meta[kw.arg] = True
|
|
94
|
+
|
|
95
|
+
if q_name:
|
|
96
|
+
producers.append(
|
|
97
|
+
AsyncProducerRecord(
|
|
98
|
+
system=AsyncMessagingSystem.AWS_SQS,
|
|
99
|
+
destination=q_name,
|
|
100
|
+
queue_id=f"aws_sqs:{q_name}",
|
|
101
|
+
caller_canonical_id=self.current_function,
|
|
102
|
+
file_path=file_path,
|
|
103
|
+
line=node.lineno,
|
|
104
|
+
call_snippet=get_line_snippet(node.lineno),
|
|
105
|
+
confidence="HIGH",
|
|
106
|
+
metadata=meta,
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Consumer: receive_message
|
|
111
|
+
elif attr == "receive_message":
|
|
112
|
+
q_name = ""
|
|
113
|
+
for kw in node.keywords:
|
|
114
|
+
if kw.arg == "QueueUrl" and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
|
|
115
|
+
q_name = _extract_queue_name(kw.value.value)
|
|
116
|
+
if q_name:
|
|
117
|
+
consumers.append(
|
|
118
|
+
AsyncConsumerRecord(
|
|
119
|
+
system=AsyncMessagingSystem.AWS_SQS,
|
|
120
|
+
destination=q_name,
|
|
121
|
+
queue_id=f"aws_sqs:{q_name}",
|
|
122
|
+
handler_canonical_id=self.current_function,
|
|
123
|
+
file_path=file_path,
|
|
124
|
+
line=node.lineno,
|
|
125
|
+
handler_snippet=get_line_snippet(node.lineno),
|
|
126
|
+
confidence="HIGH",
|
|
127
|
+
metadata={"method": "receive_message"},
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
self.generic_visit(node)
|
|
132
|
+
|
|
133
|
+
SQSVisitor().visit(tree)
|
|
134
|
+
|
|
135
|
+
return producers, consumers
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Data models for asynchronous message queues, task dispatches, and cross-service traces.
|
|
2
|
+
|
|
3
|
+
Epistemic Invariants:
|
|
4
|
+
1. Never collapse distributed message passing into a generic synchronous CALLS edge.
|
|
5
|
+
2. Distinct semantic relationships:
|
|
6
|
+
- PRODUCES_TO_QUEUE: Producer AST call site -> Queue/Topic entity
|
|
7
|
+
- CONSUMES_FROM_QUEUE: Queue/Topic entity -> Consumer Worker AST handler
|
|
8
|
+
- STATIC_QUEUE_LINKED: Same-repository static topic match (static inference, not runtime proof)
|
|
9
|
+
- OTEL_DISTRIBUTED_VERIFIED: OpenTelemetry runtime-confirmed trace/span/link proof
|
|
10
|
+
3. Queue identity: `f"{system}:{destination}"` (system + topic/queue name).
|
|
11
|
+
4. Never persist raw message bodies or unredacted keys.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import asdict, dataclass, field
|
|
16
|
+
from enum import StrEnum
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AsyncMessagingSystem(StrEnum):
|
|
21
|
+
"""Supported message brokers and asynchronous execution systems."""
|
|
22
|
+
|
|
23
|
+
KAFKA = "kafka"
|
|
24
|
+
CELERY = "celery"
|
|
25
|
+
AWS_SQS = "aws_sqs"
|
|
26
|
+
RABBITMQ = "rabbitmq"
|
|
27
|
+
REDIS = "redis"
|
|
28
|
+
GENERIC = "generic"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AsyncQueueEvidenceClass(StrEnum):
|
|
32
|
+
"""Epistemic evidence classification for asynchronous queue links."""
|
|
33
|
+
|
|
34
|
+
STATIC_QUEUE_LINKED = "STATIC_QUEUE_LINKED"
|
|
35
|
+
OTEL_DISTRIBUTED_VERIFIED = "OTEL_DISTRIBUTED_VERIFIED"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AsyncOperationType(StrEnum):
|
|
39
|
+
"""Operation performed on the message broker."""
|
|
40
|
+
|
|
41
|
+
PUBLISH = "publish"
|
|
42
|
+
CONSUME = "consume"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class AsyncQueueEntity:
|
|
47
|
+
"""Logical queue, topic, or task channel."""
|
|
48
|
+
|
|
49
|
+
queue_id: str # f"{system}:{destination}"
|
|
50
|
+
system: AsyncMessagingSystem
|
|
51
|
+
destination: str # topic name, queue URL, or celery task name
|
|
52
|
+
service: str = ""
|
|
53
|
+
environment: str = ""
|
|
54
|
+
is_dead_letter_queue: bool = False
|
|
55
|
+
|
|
56
|
+
def to_dict(self) -> dict[str, Any]:
|
|
57
|
+
return asdict(self)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class AsyncProducerRecord:
|
|
62
|
+
"""Static or runtime-observed message publisher/producer call site."""
|
|
63
|
+
|
|
64
|
+
system: AsyncMessagingSystem
|
|
65
|
+
destination: str
|
|
66
|
+
queue_id: str
|
|
67
|
+
caller_canonical_id: str
|
|
68
|
+
file_path: str
|
|
69
|
+
line: int
|
|
70
|
+
call_snippet: str
|
|
71
|
+
confidence: str = "HIGH"
|
|
72
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
73
|
+
|
|
74
|
+
def to_dict(self) -> dict[str, Any]:
|
|
75
|
+
return {
|
|
76
|
+
"system": str(self.system),
|
|
77
|
+
"destination": self.destination,
|
|
78
|
+
"queue_id": self.queue_id,
|
|
79
|
+
"caller_canonical_id": self.caller_canonical_id,
|
|
80
|
+
"file_path": self.file_path,
|
|
81
|
+
"line": self.line,
|
|
82
|
+
"call_snippet": self.call_snippet,
|
|
83
|
+
"confidence": self.confidence,
|
|
84
|
+
"metadata": dict(self.metadata),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class AsyncConsumerRecord:
|
|
90
|
+
"""Static or runtime-observed message subscriber/consumer worker handler."""
|
|
91
|
+
|
|
92
|
+
system: AsyncMessagingSystem
|
|
93
|
+
destination: str
|
|
94
|
+
queue_id: str
|
|
95
|
+
handler_canonical_id: str
|
|
96
|
+
file_path: str
|
|
97
|
+
line: int
|
|
98
|
+
handler_snippet: str
|
|
99
|
+
confidence: str = "HIGH"
|
|
100
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
101
|
+
|
|
102
|
+
def to_dict(self) -> dict[str, Any]:
|
|
103
|
+
return {
|
|
104
|
+
"system": str(self.system),
|
|
105
|
+
"destination": self.destination,
|
|
106
|
+
"queue_id": self.queue_id,
|
|
107
|
+
"handler_canonical_id": self.handler_canonical_id,
|
|
108
|
+
"file_path": self.file_path,
|
|
109
|
+
"line": self.line,
|
|
110
|
+
"handler_snippet": self.handler_snippet,
|
|
111
|
+
"confidence": self.confidence,
|
|
112
|
+
"metadata": dict(self.metadata),
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True)
|
|
117
|
+
class AsyncQueueLink:
|
|
118
|
+
"""Bridged relationship between a producer and a consumer across an async boundary."""
|
|
119
|
+
|
|
120
|
+
link_id: str
|
|
121
|
+
queue_id: str
|
|
122
|
+
system: AsyncMessagingSystem
|
|
123
|
+
destination: str
|
|
124
|
+
producer_id: str
|
|
125
|
+
producer_file: str
|
|
126
|
+
producer_line: int
|
|
127
|
+
consumer_id: str
|
|
128
|
+
consumer_file: str
|
|
129
|
+
consumer_line: int
|
|
130
|
+
evidence_class: AsyncQueueEvidenceClass
|
|
131
|
+
confidence: str = "HIGH"
|
|
132
|
+
trace_count: int = 1
|
|
133
|
+
avg_latency_ms: float = 0.0
|
|
134
|
+
p50_ms: float = 0.0
|
|
135
|
+
p95_ms: float = 0.0
|
|
136
|
+
p99_ms: float = 0.0
|
|
137
|
+
error_count: int = 0
|
|
138
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
139
|
+
|
|
140
|
+
def to_dict(self) -> dict[str, Any]:
|
|
141
|
+
return {
|
|
142
|
+
"link_id": self.link_id,
|
|
143
|
+
"queue_id": self.queue_id,
|
|
144
|
+
"system": str(self.system),
|
|
145
|
+
"destination": self.destination,
|
|
146
|
+
"producer_id": self.producer_id,
|
|
147
|
+
"producer_file": self.producer_file,
|
|
148
|
+
"producer_line": self.producer_line,
|
|
149
|
+
"consumer_id": self.consumer_id,
|
|
150
|
+
"consumer_file": self.consumer_file,
|
|
151
|
+
"consumer_line": self.consumer_line,
|
|
152
|
+
"evidence_class": str(self.evidence_class),
|
|
153
|
+
"confidence": self.confidence,
|
|
154
|
+
"trace_count": self.trace_count,
|
|
155
|
+
"avg_latency_ms": self.avg_latency_ms,
|
|
156
|
+
"p50_ms": self.p50_ms,
|
|
157
|
+
"p95_ms": self.p95_ms,
|
|
158
|
+
"p99_ms": self.p99_ms,
|
|
159
|
+
"error_count": self.error_count,
|
|
160
|
+
"metadata": dict(self.metadata),
|
|
161
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Canonical OpenTelemetry models and reconciliation classifications.
|
|
2
|
+
|
|
3
|
+
Epistemic Invariant:
|
|
4
|
+
1. Static and runtime evidence are distinct:
|
|
5
|
+
- STATIC_QUEUE_LINKED: Same-repo static topic match.
|
|
6
|
+
- OTEL_DISTRIBUTED_VERIFIED: Real empirical telemetry links producer and consumer.
|
|
7
|
+
- RUNTIME_ONLY_OBSERVED: Observed in telemetry, but consumer or producer lives in another repository/service.
|
|
8
|
+
- STATIC_RUNTIME_CONFLICT: Static code points to queue X, but runtime points to queue Y or reports persistent failure.
|
|
9
|
+
- UNKNOWN: Inconclusive / unobserved.
|
|
10
|
+
2. Zero payload retention: Never persist message bodies, tokens, or raw sensitive headers.
|
|
11
|
+
3. Correlation IDs are stored strictly as salted SHA-256 digests.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import asdict, dataclass, field
|
|
16
|
+
from enum import StrEnum
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OTelEvidenceStatus(StrEnum):
|
|
21
|
+
"""Epistemic evidence classification for static and distributed links."""
|
|
22
|
+
|
|
23
|
+
STATIC_QUEUE_LINKED = "STATIC_QUEUE_LINKED"
|
|
24
|
+
OTEL_DISTRIBUTED_VERIFIED = "OTEL_DISTRIBUTED_VERIFIED"
|
|
25
|
+
RUNTIME_ONLY_OBSERVED = "RUNTIME_ONLY_OBSERVED"
|
|
26
|
+
STATIC_RUNTIME_CONFLICT = "STATIC_RUNTIME_CONFLICT"
|
|
27
|
+
UNKNOWN = "UNKNOWN"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class OTelSpanRecord:
|
|
32
|
+
"""Sanitized, canonical representation of an ingested OpenTelemetry span."""
|
|
33
|
+
|
|
34
|
+
trace_id: str
|
|
35
|
+
span_id: str
|
|
36
|
+
parent_span_id: str = ""
|
|
37
|
+
service_name: str = ""
|
|
38
|
+
name: str = ""
|
|
39
|
+
kind: str = "INTERNAL" # PRODUCER, CONSUMER, SERVER, CLIENT, INTERNAL
|
|
40
|
+
start_time_unix_nano: int = 0
|
|
41
|
+
end_time_unix_nano: int = 0
|
|
42
|
+
duration_ms: float = 0.0
|
|
43
|
+
status_code: str = "OK" # OK, ERROR, UNSET
|
|
44
|
+
messaging_system: str = ""
|
|
45
|
+
messaging_destination: str = ""
|
|
46
|
+
messaging_operation: str = "" # publish, process, receive
|
|
47
|
+
correlation_id_hash: str = "" # Salted SHA-256 digest
|
|
48
|
+
attributes: dict[str, Any] = field(default_factory=dict)
|
|
49
|
+
links: tuple[tuple[str, str], ...] = () # ((trace_id, span_id), ...)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def composite_id(self) -> str:
|
|
53
|
+
"""Stable idempotent key for deduplication."""
|
|
54
|
+
return f"{self.trace_id}:{self.span_id}"
|
|
55
|
+
|
|
56
|
+
def to_dict(self) -> dict[str, Any]:
|
|
57
|
+
d = asdict(self)
|
|
58
|
+
d["links"] = [{"trace_id": t, "span_id": s} for t, s in self.links]
|
|
59
|
+
return d
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class TraceReconciliationResult:
|
|
64
|
+
"""Summary of an idempotent OTel trace ingestion and reconciliation pass."""
|
|
65
|
+
|
|
66
|
+
spans_ingested: int
|
|
67
|
+
spans_deduplicated: int
|
|
68
|
+
links_upgraded: int
|
|
69
|
+
links_created_runtime_only: int
|
|
70
|
+
conflicts_detected: int
|
|
71
|
+
details: list[dict[str, Any]] = field(default_factory=list)
|
|
72
|
+
|
|
73
|
+
def to_dict(self) -> dict[str, Any]:
|
|
74
|
+
return asdict(self)
|