codegraph-engine 2.3.0__py3-none-any.whl → 3.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.
Files changed (118) hide show
  1. codegraph/__init__.py +1 -1
  2. codegraph/__main__.py +7 -0
  3. codegraph/agent_brain.py +253 -2
  4. codegraph/agent_capabilities.py +1348 -67
  5. codegraph/agent_rules.py +2 -1
  6. codegraph/api_drift.py +455 -0
  7. codegraph/architecture_rules.py +123 -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/change_impact.py +320 -110
  22. codegraph/changelog.py +170 -0
  23. codegraph/cli.py +1605 -62
  24. codegraph/cli_output.py +17 -2
  25. codegraph/context.py +135 -24
  26. codegraph/context_freshness.py +2 -0
  27. codegraph/database/__init__.py +14 -0
  28. codegraph/database/extractor.py +712 -17
  29. codegraph/database/interrogation.py +388 -2
  30. codegraph/database/schema_drift.py +862 -0
  31. codegraph/dataflow_propagation.py +118 -0
  32. codegraph/dead_code.py +387 -0
  33. codegraph/epistemic.py +15 -0
  34. codegraph/evidence_contract.py +34 -3
  35. codegraph/federation.py +245 -0
  36. codegraph/frameworks.py +234 -12
  37. codegraph/frameworks_nestjs.py +502 -0
  38. codegraph/frameworks_nextjs.py +337 -0
  39. codegraph/frameworks_react.py +457 -0
  40. codegraph/freshness.py +2 -0
  41. codegraph/frontend_html.py +109 -0
  42. codegraph/git.py +2 -0
  43. codegraph/git_state.py +2 -0
  44. codegraph/graph/traversal.py +150 -7
  45. codegraph/graph_csr.py +218 -0
  46. codegraph/incremental.py +150 -0
  47. codegraph/indexing/__init__.py +7 -1
  48. codegraph/indexing/classifier.py +5 -0
  49. codegraph/indexing/indexer.py +292 -38
  50. codegraph/indexing/languages/__init__.py +1 -0
  51. codegraph/indexing/languages/cpp.py +457 -0
  52. codegraph/indexing/languages/go.py +459 -0
  53. codegraph/indexing/languages/jvm.py +583 -0
  54. codegraph/indexing/languages/rust.py +507 -0
  55. codegraph/indexing/models.py +15 -1
  56. codegraph/indexing/parser.py +385 -9
  57. codegraph/indexing/pool.py +136 -0
  58. codegraph/indexing/protocols.py +220 -0
  59. codegraph/indexing/scanner.py +23 -0
  60. codegraph/indexing/templates.py +418 -0
  61. codegraph/interrogation.py +207 -39
  62. codegraph/lsp.py +318 -0
  63. codegraph/lsp_bridge/__init__.py +16 -0
  64. codegraph/lsp_bridge/bridge.py +102 -0
  65. codegraph/lsp_bridge/client.py +207 -0
  66. codegraph/lsp_bridge/models.py +71 -0
  67. codegraph/mcp/server.py +502 -29
  68. codegraph/memory/trajectory.py +282 -0
  69. codegraph/models.py +7 -0
  70. codegraph/monorepo.py +3 -0
  71. codegraph/multi_agent/__init__.py +13 -0
  72. codegraph/multi_agent/coordinator.py +229 -0
  73. codegraph/multi_agent/models.py +76 -0
  74. codegraph/optimizer.py +42 -5
  75. codegraph/polymorphism.py +95 -0
  76. codegraph/pr_review.py +296 -0
  77. codegraph/process_lifecycle.py +50 -12
  78. codegraph/reachability.py +198 -0
  79. codegraph/reachability_index.py +74 -0
  80. codegraph/refactor/__init__.py +48 -0
  81. codegraph/refactor/models.py +114 -0
  82. codegraph/refactor/patcher.py +348 -0
  83. codegraph/refactor/renamer.py +844 -0
  84. codegraph/refactor/transactions.py +414 -0
  85. codegraph/repair.py +233 -0
  86. codegraph/resolver.py +1357 -161
  87. codegraph/retrieval_policy.py +22 -1
  88. codegraph/roblox.py +2174 -0
  89. codegraph/route_composer.py +7 -0
  90. codegraph/route_topology.py +172 -0
  91. codegraph/runtime/__init__.py +25 -0
  92. codegraph/runtime/collector.py +155 -0
  93. codegraph/runtime/db_watcher.py +191 -0
  94. codegraph/runtime/ingestor.py +30 -10
  95. codegraph/runtime/otel_receiver.py +208 -0
  96. codegraph/runtime/port_inspector.py +304 -0
  97. codegraph/runtime/runner.py +155 -6
  98. codegraph/runtime/trace_linker.py +175 -0
  99. codegraph/sandbox.py +215 -0
  100. codegraph/search/hybrid.py +26 -0
  101. codegraph/security/supply_chain.py +188 -0
  102. codegraph/security/taint.py +421 -0
  103. codegraph/semantic_decorators.py +34 -3
  104. codegraph/structural_diff.py +2 -0
  105. codegraph/styles.py +144 -0
  106. codegraph/symbol_history.py +2 -0
  107. codegraph/test_gen.py +437 -0
  108. codegraph/tsconfig.py +236 -0
  109. codegraph/ui/__init__.py +6 -0
  110. codegraph/ui/server.py +876 -0
  111. codegraph/watcher.py +323 -0
  112. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/METADATA +365 -67
  113. codegraph_engine-3.0.1.dist-info/RECORD +164 -0
  114. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/WHEEL +1 -1
  115. codegraph_engine-2.3.0.dist-info/RECORD +0 -94
  116. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/entry_points.txt +0 -0
  117. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/licenses/LICENSE +0 -0
  118. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,198 @@
1
+ """DI-Aware Deep Reachability & True Dead Code Tree-Shaking Engine for CodeGraph.
2
+
3
+ Traces downward from all legitimate repository entrypoint roots:
4
+ - HTTP Routes (FastAPI, Express, Spring, Gin, Axum, Crow, etc.)
5
+ - CLI Commands and main() entrypoints
6
+ - Scheduled tasks, Celery / async workers, and event handlers
7
+ - Traverses lexical calls, inheritance overrides, and Dependency Injection providers (@Autowired, Depends)
8
+ - Identifies true unreachable dead code with high confidence
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import sqlite3
13
+ from collections import deque
14
+ from dataclasses import asdict, dataclass, field
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class UnreachableSymbol:
21
+ """A symbol confirmed to be unreachable from any repository entrypoint."""
22
+
23
+ canonical_id: str
24
+ name: str
25
+ kind: str
26
+ file_path: str
27
+ line: int
28
+ confidence: str = "STATIC_VERIFIED"
29
+
30
+ def as_dict(self) -> dict[str, Any]:
31
+ return asdict(self)
32
+
33
+
34
+ @dataclass
35
+ class ReachabilityReport:
36
+ """Consolidated report of reachable vs dead code across the repository."""
37
+
38
+ workspace: str
39
+ total_symbols: int
40
+ reachable_count: int
41
+ unreachable_count: int
42
+ root_entrypoints_count: int
43
+ dead_code_percentage: float
44
+ unreachable_symbols: list[UnreachableSymbol] = field(default_factory=list)
45
+
46
+ def as_dict(self) -> dict[str, Any]:
47
+ return {
48
+ "workspace": self.workspace,
49
+ "total_symbols": self.total_symbols,
50
+ "reachable_count": self.reachable_count,
51
+ "unreachable_count": self.unreachable_count,
52
+ "root_entrypoints_count": self.root_entrypoints_count,
53
+ "dead_code_percentage": round(self.dead_code_percentage, 2),
54
+ "unreachable_symbols": [s.as_dict() for s in self.unreachable_symbols[:100]],
55
+ }
56
+
57
+
58
+ def compute_di_aware_reachability(
59
+ repository: Path,
60
+ con: sqlite3.Connection,
61
+ ) -> ReachabilityReport:
62
+ """Compute reachability from roots through calls, inheritance, and DI bindings."""
63
+ repo = repository.resolve()
64
+ cur = con.cursor()
65
+
66
+ # 1. Fetch all symbols
67
+ cur.execute("SELECT id, name, canonical_id, kind, path, start_line FROM symbols;")
68
+ sym_rows = cur.fetchall()
69
+ all_symbols: dict[str, tuple[str, str, str, str, int]] = {}
70
+ for r in sym_rows:
71
+ cid = str(r[2]) if r[2] else str(r[0])
72
+ # (name, canonical_id, kind, file_path, line)
73
+ all_symbols[cid] = (str(r[1]), cid, str(r[3]), str(r[4]), int(r[5]))
74
+
75
+ if not all_symbols:
76
+ return ReachabilityReport(
77
+ workspace=str(repo),
78
+ total_symbols=0,
79
+ reachable_count=0,
80
+ unreachable_count=0,
81
+ root_entrypoints_count=0,
82
+ dead_code_percentage=0.0,
83
+ )
84
+
85
+ # 2. Collect root entrypoints
86
+ # Routes
87
+ root_nodes: set[str] = set()
88
+ cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name IN ('framework_routes', 'routes') ORDER BY (name='framework_routes') DESC LIMIT 1;")
89
+ tbl_row = cur.fetchone()
90
+ if tbl_row:
91
+ routes_tbl = tbl_row[0]
92
+ cur.execute(f"SELECT handler_name, handler_canonical_id FROM {routes_tbl};")
93
+ for hname, hcid in cur.fetchall():
94
+ if hcid and hcid in all_symbols:
95
+ root_nodes.add(hcid)
96
+ elif hname:
97
+ for cid, data in all_symbols.items():
98
+ if data[0] == hname:
99
+ root_nodes.add(cid)
100
+
101
+ # CLI / Main entrypoints
102
+ for cid, data in all_symbols.items():
103
+ name_lower = data[0].lower()
104
+ if name_lower in ("main", "run", "cli", "app") or "cli" in data[3].lower():
105
+ root_nodes.add(cid)
106
+
107
+ # 3. Build Adjacency Graph from calls, inheritance, and DI bindings
108
+ adj: dict[str, set[str]] = {cid: set() for cid in all_symbols}
109
+
110
+ # Calls
111
+ cur.execute("PRAGMA table_info(calls);")
112
+ c_cols = {col[1] for col in cur.fetchall()}
113
+ if "source_symbol_id" in c_cols:
114
+ cur.execute("""
115
+ SELECT COALESCE(s.canonical_id, calls.source_symbol_id), calls.callee
116
+ FROM calls
117
+ LEFT JOIN symbols s ON (calls.source_symbol_id = s.id OR calls.source_symbol_id = s.canonical_id);
118
+ """)
119
+ else:
120
+ caller_col = "caller_canonical_id" if "caller_canonical_id" in c_cols else "source_path"
121
+ cur.execute(f"SELECT {caller_col}, callee FROM calls;")
122
+ for caller_cid, callee in cur.fetchall():
123
+ if caller_cid and caller_cid in adj and callee:
124
+ # Link to matching callee canonical_ids
125
+ for cid, data in all_symbols.items():
126
+ if data[0] == callee:
127
+ adj[caller_cid].add(cid)
128
+
129
+ # Inheritance: base class is reachable if subclass is used
130
+ try:
131
+ cur.execute("SELECT source_canonical_id, base_name FROM inheritance;")
132
+ for sub_cid, base_name in cur.fetchall():
133
+ if sub_cid and sub_cid in adj and base_name:
134
+ for cid, data in all_symbols.items():
135
+ if data[0] == base_name:
136
+ adj[sub_cid].add(cid)
137
+ except sqlite3.OperationalError:
138
+ pass
139
+
140
+ # Dependency Injection: if a service is injected, its provider is reached
141
+ cur.execute(
142
+ "SELECT name FROM sqlite_master WHERE type='table' AND name IN ('local_bindings', 'bindings') "
143
+ "ORDER BY (name='local_bindings') DESC LIMIT 1;"
144
+ )
145
+ b_row = cur.fetchone()
146
+ if b_row:
147
+ try:
148
+ cur.execute(f"SELECT scope, target_name FROM {b_row[0]} WHERE expr_kind LIKE '%DEPENDENCY%';")
149
+ for scope_cid, target_name in cur.fetchall():
150
+ if scope_cid and scope_cid in adj and target_name:
151
+ for cid, data in all_symbols.items():
152
+ if data[0].lower() == target_name.lower():
153
+ adj[scope_cid].add(cid)
154
+ except sqlite3.OperationalError:
155
+ pass
156
+
157
+ # 4. BFS Traversal from roots
158
+ reachable: set[str] = set()
159
+ queue = deque(root_nodes)
160
+ while queue:
161
+ curr = queue.popleft()
162
+ if curr in reachable:
163
+ continue
164
+ reachable.add(curr)
165
+ for neighbor in adj.get(curr, set()):
166
+ if neighbor not in reachable:
167
+ queue.append(neighbor)
168
+
169
+ # 5. Calculate dead code
170
+ unreachable_list: list[UnreachableSymbol] = []
171
+ for cid, (name, canon, kind, path, line) in all_symbols.items():
172
+ # Exclude tests and migrations from being counted as dead code
173
+ if cid not in reachable:
174
+ if not any(part in path.lower() for part in ("test", "spec", "migration", "fixture")):
175
+ unreachable_list.append(
176
+ UnreachableSymbol(
177
+ canonical_id=canon,
178
+ name=name,
179
+ kind=kind,
180
+ file_path=path,
181
+ line=line,
182
+ confidence="STATIC_VERIFIED",
183
+ )
184
+ )
185
+
186
+ total = len(all_symbols)
187
+ unreachable_count = len(unreachable_list)
188
+ dead_pct = (unreachable_count / total * 100.0) if total > 0 else 0.0
189
+
190
+ return ReachabilityReport(
191
+ workspace=str(repo),
192
+ total_symbols=total,
193
+ reachable_count=len(reachable),
194
+ unreachable_count=unreachable_count,
195
+ root_entrypoints_count=len(root_nodes),
196
+ dead_code_percentage=dead_pct,
197
+ unreachable_symbols=unreachable_list,
198
+ )
@@ -0,0 +1,74 @@
1
+ """O(1) Transitive Reachability Bitset Index for CodeGraph.
2
+
3
+ Provides constant-time reachability queries using precomputed bitmask matrices:
4
+ - can_reach(source, target) evaluates in O(1) time via bitwise AND
5
+ - Eliminates recursive graph searches during blast-radius pre-flight checks
6
+ - Instant ancestor and descendant set intersections
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+
12
+ from codegraph.graph_csr import CSRGraph
13
+
14
+
15
+ @dataclass
16
+ class TransitiveReachabilityIndex:
17
+ """Precomputed bitmask index for O(1) reachability tests."""
18
+
19
+ node_count: int
20
+ node_to_id: dict[str, int]
21
+ id_to_node: list[str]
22
+ bitsets: list[int] # Python arbitrary-precision int used as high-speed bitset
23
+
24
+ @classmethod
25
+ def build_from_csr(cls, csr: CSRGraph, max_nodes: int = 5000) -> TransitiveReachabilityIndex:
26
+ """Compute transitive reachability bitsets via topological or BFS propagation."""
27
+ n = min(csr.node_count, max_nodes)
28
+ bitsets = [0] * n
29
+
30
+ # Initialize self reachability and direct neighbors
31
+ for i in range(n):
32
+ mask = 1 << i
33
+ start = csr.indptr[i]
34
+ end = csr.indptr[i + 1]
35
+ for edge_idx in range(start, end):
36
+ neighbor = csr.indices[edge_idx]
37
+ if neighbor < n:
38
+ mask |= (1 << neighbor)
39
+ bitsets[i] = mask
40
+
41
+ # Floyd-Warshall / transitive closure propagation over bitsets
42
+ for k in range(n):
43
+ k_mask = bitsets[k]
44
+ k_bit = 1 << k
45
+ for i in range(n):
46
+ if bitsets[i] & k_bit:
47
+ bitsets[i] |= k_mask
48
+
49
+ return cls(
50
+ node_count=n,
51
+ node_to_id={csr.id_to_node[i]: i for i in range(n)},
52
+ id_to_node=csr.id_to_node[:n],
53
+ bitsets=bitsets,
54
+ )
55
+
56
+ def can_reach(self, source_node: str, target_node: str) -> bool:
57
+ """Evaluate if source_node can reach target_node in O(1) bitwise AND."""
58
+ s_id = self.node_to_id.get(source_node)
59
+ t_id = self.node_to_id.get(target_node)
60
+ if s_id is None or t_id is None:
61
+ return False
62
+ return bool(self.bitsets[s_id] & (1 << t_id))
63
+
64
+ def get_descendants(self, source_node: str) -> list[str]:
65
+ """Return all nodes reachable from source in constant-time bit iteration."""
66
+ s_id = self.node_to_id.get(source_node)
67
+ if s_id is None:
68
+ return []
69
+ mask = self.bitsets[s_id]
70
+ reachable = []
71
+ for i in range(self.node_count):
72
+ if mask & (1 << i):
73
+ reachable.append(self.id_to_node[i])
74
+ return reachable
@@ -0,0 +1,48 @@
1
+ """Deterministic, syntax-validated, transaction-safe refactoring engine for CodeGraph MCP."""
2
+ from __future__ import annotations
3
+
4
+ from codegraph.refactor.models import (
5
+ RefactorPlan,
6
+ RefactorResult,
7
+ RefactorRisk,
8
+ RefactorStatus,
9
+ TokenReplacementSpan,
10
+ )
11
+ from codegraph.refactor.patcher import (
12
+ FilePatchSpec,
13
+ PatchHunk,
14
+ apply_semantic_patch,
15
+ plan_semantic_patch,
16
+ preview_semantic_patch,
17
+ )
18
+ from codegraph.refactor.renamer import (
19
+ execute_safe_rename,
20
+ plan_safe_rename,
21
+ preview_safe_rename,
22
+ )
23
+ from codegraph.refactor.transactions import (
24
+ RefactorLock,
25
+ RefactorLockError,
26
+ commit_refactor_transaction,
27
+ rollback_refactor,
28
+ )
29
+
30
+ __all__ = [
31
+ "FilePatchSpec",
32
+ "PatchHunk",
33
+ "RefactorLock",
34
+ "RefactorLockError",
35
+ "RefactorPlan",
36
+ "RefactorResult",
37
+ "RefactorRisk",
38
+ "RefactorStatus",
39
+ "TokenReplacementSpan",
40
+ "apply_semantic_patch",
41
+ "commit_refactor_transaction",
42
+ "execute_safe_rename",
43
+ "plan_safe_rename",
44
+ "plan_semantic_patch",
45
+ "preview_safe_rename",
46
+ "preview_semantic_patch",
47
+ "rollback_refactor",
48
+ ]
@@ -0,0 +1,114 @@
1
+ """Models and data structures for deterministic, syntax-validated, transaction-safe refactoring."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import asdict, dataclass, field
5
+ from enum import StrEnum
6
+ from typing import Any
7
+
8
+
9
+ class RefactorStatus(StrEnum):
10
+ """Execution status of a refactoring plan or transaction."""
11
+
12
+ READY = "READY"
13
+ BLOCKED = "BLOCKED"
14
+ APPLIED = "APPLIED"
15
+ ROLLED_BACK = "ROLLED_BACK"
16
+ ERROR = "ERROR"
17
+
18
+
19
+ class RefactorRisk(StrEnum):
20
+ """Assessed risk of an AST refactoring operation."""
21
+
22
+ LOW = "LOW" # All affected relationships are FACT / deterministically verified.
23
+ MEDIUM = "MEDIUM" # POSSIBLE relationships exist but scope is bounded.
24
+ HIGH = "HIGH" # UNKNOWN / AMBIGUOUS / CONFLICT relationships could make rename incomplete.
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class TokenReplacementSpan:
29
+ """Exact byte/token coordinates for an AST-grounded replacement."""
30
+
31
+ path: str
32
+ start_line: int
33
+ start_col: int
34
+ end_line: int
35
+ end_col: int
36
+ old_token: str
37
+ new_token: str
38
+ evidence: str
39
+ symbol_id: str
40
+
41
+ def to_dict(self) -> dict[str, Any]:
42
+ return asdict(self)
43
+
44
+
45
+ @dataclass
46
+ class RefactorPlan:
47
+ """Deterministic refactoring plan before in-memory preview or commit."""
48
+
49
+ transaction_id: str
50
+ target_symbol: str
51
+ new_name: str
52
+ files: list[str] = field(default_factory=list)
53
+ spans: list[TokenReplacementSpan] = field(default_factory=list)
54
+ risk: RefactorRisk = RefactorRisk.LOW
55
+ uncertainty_reasons: list[str] = field(default_factory=list)
56
+ affected_tests: list[str] = field(default_factory=list)
57
+ affected_routes: list[str] = field(default_factory=list)
58
+ affected_db_relationships: list[str] = field(default_factory=list)
59
+ original_file_hashes: dict[str, str] = field(default_factory=dict)
60
+ original_file_sizes: dict[str, int] = field(default_factory=dict)
61
+ original_file_mtimes: dict[str, float] = field(default_factory=dict)
62
+
63
+ def to_dict(self) -> dict[str, Any]:
64
+ return {
65
+ "transaction_id": self.transaction_id,
66
+ "target_symbol": self.target_symbol,
67
+ "new_name": self.new_name,
68
+ "files": sorted(self.files),
69
+ "spans": [s.to_dict() for s in sorted(self.spans, key=lambda x: (x.path, x.start_line, x.start_col))],
70
+ "risk": str(self.risk),
71
+ "uncertainty_reasons": sorted(self.uncertainty_reasons),
72
+ "affected_tests": sorted(self.affected_tests),
73
+ "affected_routes": sorted(self.affected_routes),
74
+ "affected_db_relationships": sorted(self.affected_db_relationships),
75
+ "original_file_hashes": dict(sorted(self.original_file_hashes.items())),
76
+ }
77
+
78
+
79
+ @dataclass
80
+ class RefactorResult:
81
+ """Complete result of a refactor preview, apply, or rollback."""
82
+
83
+ status: RefactorStatus
84
+ target_symbol: str
85
+ new_name: str
86
+ risk: RefactorRisk
87
+ uncertainty_reasons: list[str] = field(default_factory=list)
88
+ files_changed: int = 0
89
+ symbols_changed: int = 0
90
+ spans_count: int = 0
91
+ tests_affected: list[str] = field(default_factory=list)
92
+ routes_affected: list[str] = field(default_factory=list)
93
+ db_affected: list[str] = field(default_factory=list)
94
+ rollback_id: str | None = None
95
+ diffs: dict[str, str] = field(default_factory=dict)
96
+ errors: list[str] = field(default_factory=list)
97
+
98
+ def to_dict(self) -> dict[str, Any]:
99
+ return {
100
+ "status": str(self.status),
101
+ "target_symbol": self.target_symbol,
102
+ "new_name": self.new_name,
103
+ "risk": str(self.risk),
104
+ "uncertainty_reasons": sorted(self.uncertainty_reasons),
105
+ "files_changed": self.files_changed,
106
+ "symbols_changed": self.symbols_changed,
107
+ "spans_count": self.spans_count,
108
+ "tests_affected": sorted(self.tests_affected),
109
+ "routes_affected": sorted(self.routes_affected),
110
+ "db_affected": sorted(self.db_affected),
111
+ "rollback_id": self.rollback_id,
112
+ "diffs": dict(sorted(self.diffs.items())),
113
+ "errors": sorted(self.errors),
114
+ }