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.
- codegraph/__init__.py +37 -0
- codegraph/agent.py +26 -0
- codegraph/agent_brain.py +2212 -0
- codegraph/agent_capabilities.py +4058 -0
- codegraph/agent_rules.py +531 -0
- codegraph/api_drift.py +455 -0
- codegraph/architecture.py +371 -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/audit.py +106 -0
- codegraph/binding_resolver.py +616 -0
- codegraph/branch_comparison.py +197 -0
- codegraph/cache.py +95 -0
- codegraph/change_impact.py +547 -0
- codegraph/cli.py +2436 -0
- codegraph/cli_output.py +205 -0
- codegraph/config.py +45 -0
- codegraph/constraints.py +238 -0
- codegraph/context.py +2182 -0
- codegraph/context_freshness.py +351 -0
- codegraph/database/__init__.py +72 -0
- codegraph/database/extractor.py +3651 -0
- codegraph/database/interrogation.py +1494 -0
- codegraph/database/models.py +310 -0
- codegraph/database/schema_drift.py +862 -0
- codegraph/epistemic.py +200 -0
- codegraph/errors.py +282 -0
- codegraph/evidence/__init__.py +15 -0
- codegraph/evidence/citations.py +418 -0
- codegraph/evidence_contract.py +486 -0
- codegraph/frameworks.py +960 -0
- codegraph/frameworks_nestjs.py +502 -0
- codegraph/frameworks_nextjs.py +337 -0
- codegraph/frameworks_react.py +457 -0
- codegraph/freshness.py +335 -0
- codegraph/frontend_html.py +109 -0
- codegraph/git.py +362 -0
- codegraph/git_state.py +545 -0
- codegraph/graph/__init__.py +46 -0
- codegraph/graph/models.py +56 -0
- codegraph/graph/traversal.py +1993 -0
- codegraph/indexing/__init__.py +24 -0
- codegraph/indexing/classifier.py +564 -0
- codegraph/indexing/indexer.py +2578 -0
- codegraph/indexing/models.py +427 -0
- codegraph/indexing/parser.py +3404 -0
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/scanner.py +211 -0
- codegraph/indexing/telemetry.py +326 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/indexing/test_framework.py +116 -0
- codegraph/installer.py +1702 -0
- codegraph/interrogation.py +2355 -0
- codegraph/llm/__init__.py +3 -0
- codegraph/llm/base.py +15 -0
- codegraph/llm/context.py +20 -0
- codegraph/mcp/__init__.py +3 -0
- codegraph/mcp/server.py +1476 -0
- codegraph/mcp_diagnostics.py +882 -0
- codegraph/memory/__init__.py +3 -0
- codegraph/memory/store.py +46 -0
- codegraph/models.py +310 -0
- codegraph/monorepo.py +925 -0
- codegraph/observability.py +262 -0
- codegraph/optimizer.py +1014 -0
- codegraph/planner.py +417 -0
- codegraph/process_lifecycle.py +1153 -0
- codegraph/py.typed +1 -0
- codegraph/query_expansion.py +199 -0
- codegraph/ranking.py +408 -0
- codegraph/refactor/__init__.py +36 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +414 -0
- codegraph/resolver.py +3968 -0
- codegraph/resources/__init__.py +46 -0
- codegraph/resources/cache.py +117 -0
- codegraph/resources/coalescer.py +83 -0
- codegraph/resources/debouncer.py +98 -0
- codegraph/resources/governor.py +219 -0
- codegraph/resources/monitor.py +102 -0
- codegraph/resources/policy.py +123 -0
- codegraph/retrieval_policy.py +320 -0
- codegraph/roblox.py +2174 -0
- codegraph/route_composer.py +363 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +51 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +692 -0
- codegraph/runtime/models.py +130 -0
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/reconciliation.py +492 -0
- codegraph/runtime/runner.py +353 -0
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/search/__init__.py +25 -0
- codegraph/search/hybrid.py +846 -0
- codegraph/search/semantic.py +28 -0
- codegraph/security/__init__.py +51 -0
- codegraph/security/paths.py +189 -0
- codegraph/security/redaction.py +633 -0
- codegraph/semantic_conflicts.py +270 -0
- codegraph/semantic_decorators.py +372 -0
- codegraph/structural_diff.py +542 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +277 -0
- codegraph/target_resolver.py +348 -0
- codegraph/task.py +637 -0
- codegraph/tool_selection_eval.py +1158 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +755 -0
- codegraph/watcher.py +323 -0
- codegraph_engine-3.0.0.dist-info/METADATA +868 -0
- codegraph_engine-3.0.0.dist-info/RECORD +130 -0
- codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
- codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
- codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
- codegraph_engine-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Repository scanner and reconciliation engine for asynchronous message queues and tasks.
|
|
2
|
+
|
|
3
|
+
Epistemic Invariant:
|
|
4
|
+
Static topic matching within the same repository produces STATIC_QUEUE_LINKED edges.
|
|
5
|
+
It NEVER claims to be runtime proof (OTEL_DISTRIBUTED_VERIFIED).
|
|
6
|
+
The graph structure maintains distinct entities:
|
|
7
|
+
Producer -> PRODUCES_TO_QUEUE -> Queue -> CONSUMES_FROM_QUEUE -> Consumer
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import ast
|
|
12
|
+
import hashlib
|
|
13
|
+
import sqlite3
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from codegraph.async_queue.detectors import run_all_detectors
|
|
17
|
+
from codegraph.async_queue.models import (
|
|
18
|
+
AsyncConsumerRecord,
|
|
19
|
+
AsyncProducerRecord,
|
|
20
|
+
AsyncQueueEntity,
|
|
21
|
+
AsyncQueueEvidenceClass,
|
|
22
|
+
)
|
|
23
|
+
from codegraph.indexing.models import normalize_module
|
|
24
|
+
|
|
25
|
+
ASYNC_SCHEMA = """
|
|
26
|
+
CREATE TABLE IF NOT EXISTS async_queues (
|
|
27
|
+
queue_id TEXT PRIMARY KEY,
|
|
28
|
+
system TEXT NOT NULL,
|
|
29
|
+
destination TEXT NOT NULL,
|
|
30
|
+
service TEXT NOT NULL DEFAULT '',
|
|
31
|
+
environment TEXT NOT NULL DEFAULT '',
|
|
32
|
+
is_dlq INTEGER NOT NULL DEFAULT 0,
|
|
33
|
+
metadata_json TEXT NOT NULL DEFAULT '{}'
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
CREATE TABLE IF NOT EXISTS async_producers (
|
|
37
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
38
|
+
queue_id TEXT NOT NULL,
|
|
39
|
+
system TEXT NOT NULL,
|
|
40
|
+
destination TEXT NOT NULL,
|
|
41
|
+
caller_canonical_id TEXT NOT NULL,
|
|
42
|
+
file_path TEXT NOT NULL,
|
|
43
|
+
line INTEGER NOT NULL,
|
|
44
|
+
call_snippet TEXT NOT NULL DEFAULT '',
|
|
45
|
+
confidence TEXT NOT NULL DEFAULT 'HIGH',
|
|
46
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
47
|
+
FOREIGN KEY(queue_id) REFERENCES async_queues(queue_id) ON DELETE CASCADE
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
CREATE TABLE IF NOT EXISTS async_consumers (
|
|
51
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
52
|
+
queue_id TEXT NOT NULL,
|
|
53
|
+
system TEXT NOT NULL,
|
|
54
|
+
destination TEXT NOT NULL,
|
|
55
|
+
handler_canonical_id TEXT NOT NULL,
|
|
56
|
+
file_path TEXT NOT NULL,
|
|
57
|
+
line INTEGER NOT NULL,
|
|
58
|
+
handler_snippet TEXT NOT NULL DEFAULT '',
|
|
59
|
+
confidence TEXT NOT NULL DEFAULT 'HIGH',
|
|
60
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
61
|
+
FOREIGN KEY(queue_id) REFERENCES async_queues(queue_id) ON DELETE CASCADE
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
CREATE TABLE IF NOT EXISTS async_links (
|
|
65
|
+
link_id TEXT PRIMARY KEY,
|
|
66
|
+
queue_id TEXT NOT NULL,
|
|
67
|
+
system TEXT NOT NULL,
|
|
68
|
+
destination TEXT NOT NULL,
|
|
69
|
+
producer_id TEXT NOT NULL,
|
|
70
|
+
producer_file TEXT NOT NULL,
|
|
71
|
+
producer_line INTEGER NOT NULL,
|
|
72
|
+
consumer_id TEXT NOT NULL,
|
|
73
|
+
consumer_file TEXT NOT NULL,
|
|
74
|
+
consumer_line INTEGER NOT NULL,
|
|
75
|
+
evidence_class TEXT NOT NULL DEFAULT 'STATIC_QUEUE_LINKED',
|
|
76
|
+
confidence TEXT NOT NULL DEFAULT 'HIGH',
|
|
77
|
+
trace_count INTEGER NOT NULL DEFAULT 1,
|
|
78
|
+
avg_latency_ms REAL NOT NULL DEFAULT 0.0,
|
|
79
|
+
p50_ms REAL NOT NULL DEFAULT 0.0,
|
|
80
|
+
p95_ms REAL NOT NULL DEFAULT 0.0,
|
|
81
|
+
p99_ms REAL NOT NULL DEFAULT 0.0,
|
|
82
|
+
error_count INTEGER NOT NULL DEFAULT 0,
|
|
83
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
84
|
+
FOREIGN KEY(queue_id) REFERENCES async_queues(queue_id) ON DELETE CASCADE
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
CREATE INDEX IF NOT EXISTS idx_async_producers_queue ON async_producers(queue_id);
|
|
88
|
+
CREATE INDEX IF NOT EXISTS idx_async_producers_caller ON async_producers(caller_canonical_id);
|
|
89
|
+
CREATE INDEX IF NOT EXISTS idx_async_consumers_queue ON async_consumers(queue_id);
|
|
90
|
+
CREATE INDEX IF NOT EXISTS idx_async_consumers_handler ON async_consumers(handler_canonical_id);
|
|
91
|
+
CREATE INDEX IF NOT EXISTS idx_async_links_queue ON async_links(queue_id);
|
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_async_links_producer ON async_links(producer_id);
|
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_async_links_consumer ON async_links(consumer_id);
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def ensure_async_tables(con: sqlite3.Connection) -> None:
|
|
98
|
+
"""Ensure async queue storage tables and indexes exist."""
|
|
99
|
+
con.executescript(ASYNC_SCHEMA)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def scan_file_for_async(
|
|
103
|
+
file_path: Path,
|
|
104
|
+
repo_root: Path,
|
|
105
|
+
) -> tuple[list[AsyncProducerRecord], list[AsyncConsumerRecord]]:
|
|
106
|
+
"""Scan a single Python file for async messaging producers and consumers."""
|
|
107
|
+
rel_path = file_path.relative_to(repo_root).as_posix()
|
|
108
|
+
module_name = normalize_module(rel_path, "python")
|
|
109
|
+
try:
|
|
110
|
+
content = file_path.read_text(encoding="utf-8", errors="replace")
|
|
111
|
+
tree = ast.parse(content, filename=rel_path)
|
|
112
|
+
except Exception:
|
|
113
|
+
return [], []
|
|
114
|
+
|
|
115
|
+
return run_all_detectors(tree, content, rel_path, module_name)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class AsyncQueueScanner:
|
|
119
|
+
"""Repository-level scanner and graph reconciler for async queues."""
|
|
120
|
+
|
|
121
|
+
def __init__(self, repository: Path) -> None:
|
|
122
|
+
self.repository = repository.resolve()
|
|
123
|
+
|
|
124
|
+
def scan_repository(
|
|
125
|
+
self,
|
|
126
|
+
con: sqlite3.Connection,
|
|
127
|
+
) -> dict[str, int]:
|
|
128
|
+
"""Scan all repository Python files, persist entities, and link matching queues."""
|
|
129
|
+
ensure_async_tables(con)
|
|
130
|
+
|
|
131
|
+
producers: list[AsyncProducerRecord] = []
|
|
132
|
+
consumers: list[AsyncConsumerRecord] = []
|
|
133
|
+
|
|
134
|
+
# Find all Python source files
|
|
135
|
+
for py_file in self.repository.rglob("*.py"):
|
|
136
|
+
if ".git" in py_file.parts or "__pycache__" in py_file.parts or ".codegraph" in py_file.parts:
|
|
137
|
+
continue
|
|
138
|
+
prods, cons = scan_file_for_async(py_file, self.repository)
|
|
139
|
+
producers.extend(prods)
|
|
140
|
+
consumers.extend(cons)
|
|
141
|
+
|
|
142
|
+
# Clear existing static async tables
|
|
143
|
+
con.execute("DELETE FROM async_links WHERE evidence_class='STATIC_QUEUE_LINKED'")
|
|
144
|
+
con.execute("DELETE FROM async_consumers")
|
|
145
|
+
con.execute("DELETE FROM async_producers")
|
|
146
|
+
con.execute("DELETE FROM async_queues")
|
|
147
|
+
con.execute("DELETE FROM graph_edges WHERE relationship IN ('PRODUCES_TO_QUEUE', 'CONSUMES_FROM_QUEUE', 'STATIC_QUEUE_LINKED')")
|
|
148
|
+
|
|
149
|
+
# Collect distinct queues
|
|
150
|
+
queues_by_id: dict[str, AsyncQueueEntity] = {}
|
|
151
|
+
for p in producers:
|
|
152
|
+
if p.queue_id not in queues_by_id:
|
|
153
|
+
queues_by_id[p.queue_id] = AsyncQueueEntity(
|
|
154
|
+
queue_id=p.queue_id,
|
|
155
|
+
system=p.system,
|
|
156
|
+
destination=p.destination,
|
|
157
|
+
)
|
|
158
|
+
for c in consumers:
|
|
159
|
+
if c.queue_id not in queues_by_id:
|
|
160
|
+
queues_by_id[c.queue_id] = AsyncQueueEntity(
|
|
161
|
+
queue_id=c.queue_id,
|
|
162
|
+
system=c.system,
|
|
163
|
+
destination=c.destination,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Insert async_queues
|
|
167
|
+
for q in queues_by_id.values():
|
|
168
|
+
con.execute(
|
|
169
|
+
"INSERT INTO async_queues (queue_id, system, destination, service, environment, is_dlq) "
|
|
170
|
+
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
171
|
+
(q.queue_id, str(q.system), q.destination, q.service, q.environment, 1 if q.is_dead_letter_queue else 0),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Insert async_producers
|
|
175
|
+
for p in producers:
|
|
176
|
+
con.execute(
|
|
177
|
+
"INSERT INTO async_producers (queue_id, system, destination, caller_canonical_id, file_path, line, call_snippet, confidence) "
|
|
178
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
179
|
+
(p.queue_id, str(p.system), p.destination, p.caller_canonical_id, p.file_path, p.line, p.call_snippet, p.confidence),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Insert async_consumers
|
|
183
|
+
for c in consumers:
|
|
184
|
+
con.execute(
|
|
185
|
+
"INSERT INTO async_consumers (queue_id, system, destination, handler_canonical_id, file_path, line, handler_snippet, confidence) "
|
|
186
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
187
|
+
(c.queue_id, str(c.system), c.destination, c.handler_canonical_id, c.file_path, c.line, c.handler_snippet, c.confidence),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Reconcile STATIC_QUEUE_LINKED edges (Same repository topic/queue matching)
|
|
191
|
+
# Groups: fan-out (1 producer -> N consumers) and fan-in (M producers -> 1 consumer)
|
|
192
|
+
prods_by_queue: dict[str, list[AsyncProducerRecord]] = {}
|
|
193
|
+
for p in producers:
|
|
194
|
+
prods_by_queue.setdefault(p.queue_id, []).append(p)
|
|
195
|
+
|
|
196
|
+
cons_by_queue: dict[str, list[AsyncConsumerRecord]] = {}
|
|
197
|
+
for c in consumers:
|
|
198
|
+
cons_by_queue.setdefault(c.queue_id, []).append(c)
|
|
199
|
+
|
|
200
|
+
links_count = 0
|
|
201
|
+
graph_edges_to_insert: list[tuple[str, str, str, str, str, int, int, str, str]] = []
|
|
202
|
+
|
|
203
|
+
for q_id, q_prods in prods_by_queue.items():
|
|
204
|
+
q_cons = cons_by_queue.get(q_id, [])
|
|
205
|
+
|
|
206
|
+
# 1. PRODUCES_TO_QUEUE edges: Producer -> Queue
|
|
207
|
+
for p in q_prods:
|
|
208
|
+
graph_edges_to_insert.append((
|
|
209
|
+
p.caller_canonical_id,
|
|
210
|
+
q_id,
|
|
211
|
+
"PRODUCES_TO_QUEUE",
|
|
212
|
+
"HIGH",
|
|
213
|
+
p.file_path,
|
|
214
|
+
p.line,
|
|
215
|
+
p.line,
|
|
216
|
+
p.call_snippet,
|
|
217
|
+
"STATIC_QUEUE_LINKED",
|
|
218
|
+
))
|
|
219
|
+
|
|
220
|
+
# 2. CONSUMES_FROM_QUEUE edges: Queue -> Consumer
|
|
221
|
+
for c in q_cons:
|
|
222
|
+
graph_edges_to_insert.append((
|
|
223
|
+
q_id,
|
|
224
|
+
c.handler_canonical_id,
|
|
225
|
+
"CONSUMES_FROM_QUEUE",
|
|
226
|
+
"HIGH",
|
|
227
|
+
c.file_path,
|
|
228
|
+
c.line,
|
|
229
|
+
c.line,
|
|
230
|
+
c.handler_snippet,
|
|
231
|
+
"STATIC_QUEUE_LINKED",
|
|
232
|
+
))
|
|
233
|
+
|
|
234
|
+
# 3. Direct bridge links: Producer -> Consumer across queue boundary
|
|
235
|
+
for p in q_prods:
|
|
236
|
+
for c in q_cons:
|
|
237
|
+
link_seed = f"{p.caller_canonical_id}:{q_id}:{c.handler_canonical_id}:{p.file_path}:{p.line}"
|
|
238
|
+
link_id = hashlib.sha256(link_seed.encode("utf-8")).hexdigest()[:16]
|
|
239
|
+
|
|
240
|
+
con.execute(
|
|
241
|
+
"INSERT OR REPLACE INTO async_links ("
|
|
242
|
+
"link_id, queue_id, system, destination, producer_id, producer_file, producer_line, "
|
|
243
|
+
"consumer_id, consumer_file, consumer_line, evidence_class, confidence"
|
|
244
|
+
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
245
|
+
(
|
|
246
|
+
link_id,
|
|
247
|
+
q_id,
|
|
248
|
+
str(p.system),
|
|
249
|
+
p.destination,
|
|
250
|
+
p.caller_canonical_id,
|
|
251
|
+
p.file_path,
|
|
252
|
+
p.line,
|
|
253
|
+
c.handler_canonical_id,
|
|
254
|
+
c.file_path,
|
|
255
|
+
c.line,
|
|
256
|
+
AsyncQueueEvidenceClass.STATIC_QUEUE_LINKED.value,
|
|
257
|
+
"HIGH",
|
|
258
|
+
),
|
|
259
|
+
)
|
|
260
|
+
links_count += 1
|
|
261
|
+
|
|
262
|
+
# Direct STATIC_QUEUE_LINKED edge in graph_edges
|
|
263
|
+
graph_edges_to_insert.append((
|
|
264
|
+
p.caller_canonical_id,
|
|
265
|
+
c.handler_canonical_id,
|
|
266
|
+
"STATIC_QUEUE_LINKED",
|
|
267
|
+
"HIGH",
|
|
268
|
+
p.file_path,
|
|
269
|
+
p.line,
|
|
270
|
+
c.line,
|
|
271
|
+
f"Matched queue '{q_id}' ({p.system})",
|
|
272
|
+
"STATIC_QUEUE_LINKED",
|
|
273
|
+
))
|
|
274
|
+
|
|
275
|
+
# Insert graph edges with ON CONFLICT IGNORE
|
|
276
|
+
if graph_edges_to_insert:
|
|
277
|
+
con.executemany(
|
|
278
|
+
"INSERT OR IGNORE INTO graph_edges ("
|
|
279
|
+
"source, target, relationship, confidence, file, start_line, end_line, evidence, evidence_class"
|
|
280
|
+
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
281
|
+
graph_edges_to_insert,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
con.commit()
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
"queues": len(queues_by_id),
|
|
288
|
+
"producers": len(producers),
|
|
289
|
+
"consumers": len(consumers),
|
|
290
|
+
"links": links_count,
|
|
291
|
+
}
|