codecortex-context-engine 0.1.0a1__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 (90) hide show
  1. codecortex/__init__.py +3 -0
  2. codecortex/architecture/__init__.py +23 -0
  3. codecortex/architecture/drift.py +182 -0
  4. codecortex/architecture/inference.py +160 -0
  5. codecortex/backends/__init__.py +35 -0
  6. codecortex/backends/base.py +38 -0
  7. codecortex/backends/context.py +118 -0
  8. codecortex/backends/contracts.py +65 -0
  9. codecortex/backends/factory.py +60 -0
  10. codecortex/backends/graph.py +107 -0
  11. codecortex/backends/manager.py +303 -0
  12. codecortex/backends/mcp_client.py +196 -0
  13. codecortex/backends/pool.py +128 -0
  14. codecortex/backends/spec.py +83 -0
  15. codecortex/backends/symbols.py +189 -0
  16. codecortex/benchmark.py +211 -0
  17. codecortex/cli.py +409 -0
  18. codecortex/config.py +27 -0
  19. codecortex/context/__init__.py +13 -0
  20. codecortex/context/budget.py +62 -0
  21. codecortex/context/integrated.py +60 -0
  22. codecortex/context/pipeline.py +223 -0
  23. codecortex/core/__init__.py +1 -0
  24. codecortex/core/contracts.py +45 -0
  25. codecortex/core/errors.py +17 -0
  26. codecortex/core/models.py +69 -0
  27. codecortex/dashboard.py +259 -0
  28. codecortex/editing.py +41 -0
  29. codecortex/engines/__init__.py +5 -0
  30. codecortex/engines/builtin/__init__.py +5 -0
  31. codecortex/engines/builtin/factory.py +26 -0
  32. codecortex/engines/builtin/memory.py +35 -0
  33. codecortex/engines/builtin/repository.py +73 -0
  34. codecortex/engines/builtin/symbols.py +89 -0
  35. codecortex/engines/builtin/validation.py +54 -0
  36. codecortex/engines/registry.py +26 -0
  37. codecortex/entrypoint.py +266 -0
  38. codecortex/evaluation/__init__.py +55 -0
  39. codecortex/evaluation/external.py +265 -0
  40. codecortex/evaluation/production.py +670 -0
  41. codecortex/evaluation/regression.py +188 -0
  42. codecortex/gateway.py +38 -0
  43. codecortex/git_intelligence.py +252 -0
  44. codecortex/indexing/__init__.py +6 -0
  45. codecortex/indexing/graph.py +78 -0
  46. codecortex/indexing/impact.py +127 -0
  47. codecortex/indexing/incremental.py +163 -0
  48. codecortex/indexing/incremental_graph.py +189 -0
  49. codecortex/indexing/indexer.py +172 -0
  50. codecortex/indexing/relationships.py +179 -0
  51. codecortex/indexing/resolution.py +88 -0
  52. codecortex/integrations/__init__.py +5 -0
  53. codecortex/integrations/agents.py +235 -0
  54. codecortex/interfaces/__init__.py +1 -0
  55. codecortex/interfaces/mcp_bridge.py +66 -0
  56. codecortex/languages/__init__.py +5 -0
  57. codecortex/languages/native.py +166 -0
  58. codecortex/languages/registry.py +232 -0
  59. codecortex/mcp/__init__.py +5 -0
  60. codecortex/mcp/extended.py +114 -0
  61. codecortex/mcp/server.py +473 -0
  62. codecortex/memory/__init__.py +11 -0
  63. codecortex/memory/json_store.py +52 -0
  64. codecortex/memory/knowledge.py +193 -0
  65. codecortex/memory/team_store.py +193 -0
  66. codecortex/orchestrator.py +153 -0
  67. codecortex/pr_intelligence.py +214 -0
  68. codecortex/retrieval/__init__.py +16 -0
  69. codecortex/retrieval/hybrid.py +67 -0
  70. codecortex/retrieval/index.py +135 -0
  71. codecortex/retrieval/providers.py +67 -0
  72. codecortex/retrieval/repository.py +94 -0
  73. codecortex/router/__init__.py +5 -0
  74. codecortex/router/router.py +79 -0
  75. codecortex/runtime.py +69 -0
  76. codecortex/setup.py +100 -0
  77. codecortex/symbols/__init__.py +5 -0
  78. codecortex/symbols/providers.py +192 -0
  79. codecortex/telemetry/__init__.py +5 -0
  80. codecortex/telemetry/collector.py +43 -0
  81. codecortex/tracing/__init__.py +9 -0
  82. codecortex/tracing/task_trace.py +235 -0
  83. codecortex/workspace/__init__.py +9 -0
  84. codecortex/workspace/federation.py +173 -0
  85. codecortex_context_engine-0.1.0a1.dist-info/METADATA +381 -0
  86. codecortex_context_engine-0.1.0a1.dist-info/RECORD +90 -0
  87. codecortex_context_engine-0.1.0a1.dist-info/WHEEL +4 -0
  88. codecortex_context_engine-0.1.0a1.dist-info/entry_points.txt +3 -0
  89. codecortex_context_engine-0.1.0a1.dist-info/licenses/LICENSE +201 -0
  90. codecortex_context_engine-0.1.0a1.dist-info/licenses/NOTICE +2 -0
@@ -0,0 +1,473 @@
1
+ """Native stateless MCP server with stdio transport."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import sys
8
+ from dataclasses import asdict
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from codecortex.architecture import (
13
+ ArchitectureDriftDetector,
14
+ ArchitectureFingerprint,
15
+ ArchitectureInferenceEngine,
16
+ )
17
+ from codecortex.context import ContextPipeline
18
+ from codecortex.git_intelligence import GitIntelligence
19
+ from codecortex.indexing.impact import ImpactAnalyzer
20
+ from codecortex.indexing.incremental_graph import IncrementalGraphIndex
21
+ from codecortex.memory import TeamMemoryStore
22
+ from codecortex.memory.knowledge import ProjectKnowledgeExtractor
23
+ from codecortex.pr_intelligence import PRIntelligence
24
+ from codecortex.retrieval import RepositorySemanticIndex
25
+ from codecortex.runtime import CortexRuntime, build_runtime
26
+ from codecortex.tracing import TaskTraceRecorder
27
+ from codecortex.workspace import MultiRepositoryWorkspace
28
+
29
+ PROTOCOL_VERSION = "2026-07-28"
30
+ SERVER_INFO = {"name": "codecortex", "version": "0.1.0"}
31
+
32
+
33
+ def _schema(properties: dict[str, Any], required: list[str] | None = None) -> dict[str, Any]:
34
+ schema: dict[str, Any] = {
35
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
36
+ "type": "object",
37
+ "properties": properties,
38
+ "additionalProperties": False,
39
+ }
40
+ if required:
41
+ schema["required"] = required
42
+ return schema
43
+
44
+
45
+ class MCPApplication:
46
+ def __init__(self, runtime: CortexRuntime) -> None:
47
+ self.runtime = runtime
48
+ self.root = runtime.config.project_root
49
+
50
+ def tools(self) -> list[dict[str, Any]]:
51
+ text = {"type": "string", "minLength": 1}
52
+ positive_int = {"type": "integer", "minimum": 1}
53
+ return [
54
+ {
55
+ "name": "cortex_repository_map",
56
+ "description": "Inspect repository graph counts and matching nodes.",
57
+ "inputSchema": _schema({"query": {"type": "string", "default": ""}}),
58
+ },
59
+ {
60
+ "name": "cortex_find_symbol",
61
+ "description": "Find code symbols across supported languages.",
62
+ "inputSchema": _schema({"query": text}, ["query"]),
63
+ },
64
+ {
65
+ "name": "cortex_find_references",
66
+ "description": "Find graph references to a symbol or file.",
67
+ "inputSchema": _schema({"query": text}, ["query"]),
68
+ },
69
+ {
70
+ "name": "cortex_dependency_graph",
71
+ "description": "Return dependency and call relationships around a target.",
72
+ "inputSchema": _schema({"query": text}, ["query"]),
73
+ },
74
+ {
75
+ "name": "cortex_impact",
76
+ "description": "Estimate impact and risk of changing a target.",
77
+ "inputSchema": _schema({"query": text}, ["query"]),
78
+ },
79
+ {
80
+ "name": "cortex_semantic_search",
81
+ "description": "Hybrid semantic, lexical, and structural repository search.",
82
+ "inputSchema": _schema(
83
+ {"query": text, "limit": {**positive_int, "default": 20}},
84
+ ["query"],
85
+ ),
86
+ },
87
+ {
88
+ "name": "cortex_context",
89
+ "description": "Build compact query-specific context.",
90
+ "inputSchema": _schema(
91
+ {
92
+ "query": text,
93
+ "budget": {"type": "integer", "minimum": 128, "default": 32000},
94
+ },
95
+ ["query"],
96
+ ),
97
+ },
98
+ {
99
+ "name": "cortex_architecture",
100
+ "description": "Infer repository architecture with evidence and confidence.",
101
+ "inputSchema": _schema({}),
102
+ },
103
+ {
104
+ "name": "cortex_architecture_drift",
105
+ "description": "Compare current architecture with a saved baseline fingerprint.",
106
+ "inputSchema": _schema({}),
107
+ },
108
+ {
109
+ "name": "cortex_symbol_history",
110
+ "description": "Return Git history, blame, and ownership for a symbol line range.",
111
+ "inputSchema": _schema(
112
+ {"path": text, "start": positive_int, "end": positive_int},
113
+ ["path", "start", "end"],
114
+ ),
115
+ },
116
+ {
117
+ "name": "cortex_pr_intelligence",
118
+ "description": "Analyze a Git diff for changed symbols, impact, tests, and risk.",
119
+ "inputSchema": _schema(
120
+ {
121
+ "base_ref": text,
122
+ "head_ref": {"type": "string", "default": "HEAD"},
123
+ },
124
+ ["base_ref"],
125
+ ),
126
+ },
127
+ {
128
+ "name": "cortex_memory_search",
129
+ "description": "Search local project memory and extracted knowledge.",
130
+ "inputSchema": _schema({"query": text}, ["query"]),
131
+ },
132
+ {
133
+ "name": "cortex_team_memory_search",
134
+ "description": "Search revisioned shared team memory.",
135
+ "inputSchema": _schema(
136
+ {
137
+ "query": text,
138
+ "namespace": {"type": "string", "default": "project"},
139
+ },
140
+ ["query"],
141
+ ),
142
+ },
143
+ {
144
+ "name": "cortex_remember",
145
+ "description": "Save a project decision or durable fact.",
146
+ "inputSchema": _schema({"key": text, "value": text}, ["key", "value"]),
147
+ },
148
+ {
149
+ "name": "cortex_workspace_search",
150
+ "description": "Search all repositories registered in the current workspace.",
151
+ "inputSchema": _schema(
152
+ {"query": text, "limit": {**positive_int, "default": 40}},
153
+ ["query"],
154
+ ),
155
+ },
156
+ {
157
+ "name": "cortex_trace_summary",
158
+ "description": "Summarize a recorded agent task trace.",
159
+ "inputSchema": _schema({"trace_id": text}, ["trace_id"]),
160
+ },
161
+ {
162
+ "name": "cortex_validate",
163
+ "description": "Run validation for a coding request.",
164
+ "inputSchema": _schema({"query": text}, ["query"]),
165
+ },
166
+ {
167
+ "name": "cortex_stats",
168
+ "description": "Return index, Git, graph, and runtime statistics.",
169
+ "inputSchema": _schema({}),
170
+ },
171
+ ]
172
+
173
+ def _graph(self):
174
+ return IncrementalGraphIndex(self.root).refresh()[0]
175
+
176
+ async def call(self, name: str, arguments: dict[str, Any]) -> dict[str, Any]:
177
+ if name == "cortex_repository_map":
178
+ graph = self._graph()
179
+ query = str(arguments.get("query", "")).strip()
180
+ matches = graph.search(query, 30) if query else []
181
+ return {
182
+ "counts": graph.counts(),
183
+ "edges": len(graph.edges),
184
+ "matches": [node.model_dump(mode="json") for node in matches],
185
+ }
186
+ if name == "cortex_find_symbol":
187
+ graph = self._graph()
188
+ matches = [
189
+ node
190
+ for node in graph.search(str(arguments["query"]), 80)
191
+ if node.kind not in {"file", "module", "reference"}
192
+ ]
193
+ return {"symbols": [node.model_dump(mode="json") for node in matches[:50]]}
194
+ if name in {"cortex_find_references", "cortex_dependency_graph"}:
195
+ graph = self._graph()
196
+ matches = graph.search(str(arguments["query"]), 3)
197
+ if not matches:
198
+ return {"nodes": [], "edges": []}
199
+ ids = {node.id for node in matches}
200
+ edges = [
201
+ edge
202
+ for edge in graph.edges
203
+ if edge.source in ids or edge.target in ids
204
+ ]
205
+ if name == "cortex_find_references":
206
+ edges = [edge for edge in edges if edge.target in ids]
207
+ connected = ids | {edge.source for edge in edges} | {edge.target for edge in edges}
208
+ nodes = [node for node in graph.nodes if node.id in connected]
209
+ return {
210
+ "nodes": [node.model_dump(mode="json") for node in nodes],
211
+ "edges": [edge.model_dump(mode="json") for edge in edges],
212
+ }
213
+ if name == "cortex_impact":
214
+ report = ImpactAnalyzer(self._graph()).analyze(str(arguments["query"]))
215
+ return {
216
+ "target": report.target.model_dump(mode="json"),
217
+ "risk_score": report.risk_score,
218
+ "direct": [self._impact_item(item) for item in report.direct],
219
+ "indirect": [self._impact_item(item) for item in report.indirect],
220
+ "affected_tests": [self._impact_item(item) for item in report.affected_tests],
221
+ }
222
+ if name == "cortex_semantic_search":
223
+ semantic = RepositorySemanticIndex(self.root)
224
+ semantic.refresh(self._graph())
225
+ hits = semantic.search(str(arguments["query"]), int(arguments.get("limit", 20)))
226
+ return {
227
+ "hits": [
228
+ {
229
+ "id": hit.document.id,
230
+ "score": hit.score,
231
+ "vector_score": hit.vector_score,
232
+ "lexical_score": hit.lexical_score,
233
+ "structural_score": hit.structural_score,
234
+ "metadata": hit.document.metadata,
235
+ }
236
+ for hit in hits
237
+ ]
238
+ }
239
+ if name == "cortex_context":
240
+ query = str(arguments["query"])
241
+ budget = int(arguments.get("budget", 32000))
242
+ execution = await self.runtime.gateway.query(query, str(self.root))
243
+ chunks = [chunk for result in execution.results for chunk in result.chunks]
244
+ prepared = await ContextPipeline(self.root, self._graph()).prepare(
245
+ query,
246
+ chunks,
247
+ budget,
248
+ )
249
+ return {
250
+ "chunks": [chunk.model_dump(mode="json") for chunk in prepared.chunks],
251
+ "metrics": asdict(prepared.metrics),
252
+ "trace_id": execution.metadata.get("trace_id"),
253
+ }
254
+ if name == "cortex_architecture":
255
+ report = ArchitectureInferenceEngine().analyze(self._graph())
256
+ return asdict(report)
257
+ if name == "cortex_architecture_drift":
258
+ detector = ArchitectureDriftDetector()
259
+ current = detector.fingerprint(self._graph())
260
+ baseline_path = self.root / ".codecortex" / "architecture" / "baseline.json"
261
+ baseline = ArchitectureFingerprint.load(baseline_path)
262
+ if baseline is None:
263
+ return {
264
+ "baseline": "missing",
265
+ "current": asdict(current),
266
+ "hint": "Create a baseline with the architecture-baseline CLI command.",
267
+ }
268
+ return {
269
+ "baseline": asdict(baseline),
270
+ "current": asdict(current),
271
+ "drift": asdict(detector.compare(baseline, current)),
272
+ }
273
+ if name == "cortex_symbol_history":
274
+ history = GitIntelligence(self.root).symbol_history(
275
+ str(arguments["path"]),
276
+ int(arguments["start"]),
277
+ int(arguments["end"]),
278
+ )
279
+ return asdict(history)
280
+ if name == "cortex_pr_intelligence":
281
+ report = PRIntelligence(self.root, self._graph()).analyze(
282
+ str(arguments["base_ref"]),
283
+ str(arguments.get("head_ref", "HEAD")),
284
+ )
285
+ return {
286
+ "base_ref": report.base_ref,
287
+ "head_ref": report.head_ref,
288
+ "risk_score": report.risk_score,
289
+ "risk_level": report.risk_level,
290
+ "affected_tests": list(report.affected_tests),
291
+ "files": [asdict(item) for item in report.files],
292
+ "symbols": [
293
+ {
294
+ "node": item.node.model_dump(mode="json"),
295
+ "impact_risk": item.impact_risk,
296
+ "affected_nodes": item.affected_nodes,
297
+ "affected_tests": item.affected_tests,
298
+ }
299
+ for item in report.symbols
300
+ ],
301
+ }
302
+ if name == "cortex_memory_search":
303
+ query = str(arguments["query"])
304
+ project = await self.runtime.memory.search("project", query, 10)
305
+ knowledge = await self.runtime.memory.search("project_knowledge", query, 10)
306
+ return {"results": [*project, *knowledge][:20]}
307
+ if name == "cortex_team_memory_search":
308
+ store = TeamMemoryStore(
309
+ self.root / ".codecortex" / "memory" / "team.sqlite3"
310
+ )
311
+ entries = store.search_entries(
312
+ str(arguments.get("namespace", "project")),
313
+ str(arguments["query"]),
314
+ 20,
315
+ )
316
+ return {"results": [asdict(entry) for entry in entries]}
317
+ if name == "cortex_remember":
318
+ await self.runtime.gateway.remember(
319
+ str(arguments["key"]),
320
+ str(arguments["value"]),
321
+ )
322
+ return {"saved": True}
323
+ if name == "cortex_workspace_search":
324
+ workspace = MultiRepositoryWorkspace(
325
+ self.root / ".codecortex" / "workspace.json"
326
+ )
327
+ hits = workspace.search(
328
+ str(arguments["query"]),
329
+ int(arguments.get("limit", 40)),
330
+ )
331
+ return {
332
+ "hits": [
333
+ {
334
+ "repository": hit.repository,
335
+ "score": hit.score,
336
+ "node": hit.node.model_dump(mode="json"),
337
+ }
338
+ for hit in hits
339
+ ]
340
+ }
341
+ if name == "cortex_trace_summary":
342
+ recorder = TaskTraceRecorder(
343
+ self.root / ".codecortex" / "runtime" / "traces.jsonl"
344
+ )
345
+ return asdict(recorder.summarize(str(arguments["trace_id"])))
346
+ if name == "cortex_validate":
347
+ result = await self.runtime.gateway.query(str(arguments["query"]), str(self.root))
348
+ return {
349
+ "validation": [
350
+ item.model_dump(mode="json")
351
+ for item in result.results
352
+ if item.capability.value == "validation"
353
+ ],
354
+ "trace_id": result.metadata.get("trace_id"),
355
+ }
356
+ if name == "cortex_stats":
357
+ graph, graph_stats = IncrementalGraphIndex(self.root).refresh()
358
+ git = GitIntelligence(self.root).analyze(300)
359
+ knowledge = ProjectKnowledgeExtractor(self.root).extract()
360
+ return {
361
+ "index": {
362
+ "tracked": graph_stats.index.tracked,
363
+ "added": len(graph_stats.index.added),
364
+ "changed": len(graph_stats.index.changed),
365
+ "removed": len(graph_stats.index.removed),
366
+ "files_reparsed": graph_stats.files_reparsed,
367
+ "full_rebuild": graph_stats.full_rebuild,
368
+ "duration_ms": graph_stats.index.duration_ms,
369
+ },
370
+ "graph": {
371
+ "nodes": len(graph.nodes),
372
+ "edges": len(graph.edges),
373
+ **graph.counts(),
374
+ },
375
+ "git": {
376
+ "commits": git.commits,
377
+ "hot_files": [item.path for item in git.hot_files[:10]],
378
+ },
379
+ "knowledge": knowledge.facts(),
380
+ "health": await self.runtime.gateway.health(),
381
+ }
382
+ raise KeyError(f"Unknown tool: {name}")
383
+
384
+ @staticmethod
385
+ def _impact_item(item: Any) -> dict[str, Any]:
386
+ return {
387
+ "node": item.node.model_dump(mode="json"),
388
+ "depth": item.depth,
389
+ "via": item.via,
390
+ "risk": item.risk,
391
+ }
392
+
393
+
394
+ class MCPServer:
395
+ def __init__(self, application: MCPApplication) -> None:
396
+ self.application = application
397
+
398
+ async def dispatch(self, message: dict[str, Any]) -> dict[str, Any] | None:
399
+ request_id = message.get("id")
400
+ method = message.get("method")
401
+ if request_id is None:
402
+ return None
403
+ try:
404
+ if method in {"server/discover", "initialize"}:
405
+ return self._result(request_id, self._discovery())
406
+ if method == "ping":
407
+ return self._result(request_id, {})
408
+ if method == "tools/list":
409
+ return self._result(
410
+ request_id,
411
+ {"tools": self.application.tools(), "ttlMs": 300000},
412
+ )
413
+ if method == "tools/call":
414
+ params = message.get("params") or {}
415
+ payload = await self.application.call(
416
+ str(params.get("name", "")),
417
+ dict(params.get("arguments") or {}),
418
+ )
419
+ return self._result(request_id, self._tool_result(payload))
420
+ return self._error(request_id, -32601, f"Method not found: {method}")
421
+ except (KeyError, TypeError, ValueError) as exc:
422
+ return self._error(request_id, -32602, str(exc))
423
+ except Exception as exc: # pragma: no cover
424
+ return self._error(request_id, -32603, f"Internal error: {exc}")
425
+
426
+ def _discovery(self) -> dict[str, Any]:
427
+ return {
428
+ "protocolVersion": PROTOCOL_VERSION,
429
+ "serverInfo": SERVER_INFO,
430
+ "capabilities": {"tools": {"listChanged": False}},
431
+ }
432
+
433
+ @staticmethod
434
+ def _tool_result(payload: dict[str, Any]) -> dict[str, Any]:
435
+ text = json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
436
+ return {
437
+ "content": [{"type": "text", "text": text}],
438
+ "structuredContent": payload,
439
+ "isError": False,
440
+ }
441
+
442
+ @staticmethod
443
+ def _result(request_id: Any, result: dict[str, Any]) -> dict[str, Any]:
444
+ return {"jsonrpc": "2.0", "id": request_id, "result": result}
445
+
446
+ @staticmethod
447
+ def _error(request_id: Any, code: int, message: str) -> dict[str, Any]:
448
+ return {
449
+ "jsonrpc": "2.0",
450
+ "id": request_id,
451
+ "error": {"code": code, "message": message},
452
+ }
453
+
454
+ async def serve_stdio(self) -> None:
455
+ loop = asyncio.get_running_loop()
456
+ while True:
457
+ line = await loop.run_in_executor(None, sys.stdin.readline)
458
+ if not line:
459
+ return
460
+ try:
461
+ message = json.loads(line)
462
+ except json.JSONDecodeError:
463
+ response = self._error(None, -32700, "Parse error")
464
+ else:
465
+ response = await self.dispatch(message)
466
+ if response is not None:
467
+ sys.stdout.write(json.dumps(response, ensure_ascii=False) + "\n")
468
+ sys.stdout.flush()
469
+
470
+
471
+ def run_stdio(project_root: Path | None = None) -> None:
472
+ runtime = build_runtime(project_root)
473
+ asyncio.run(MCPServer(MCPApplication(runtime)).serve_stdio())
@@ -0,0 +1,11 @@
1
+ """Project and team-scoped persistent memory."""
2
+
3
+ from codecortex.memory.json_store import JsonMemoryStore
4
+ from codecortex.memory.team_store import RevisionConflict, TeamMemoryEntry, TeamMemoryStore
5
+
6
+ __all__ = [
7
+ "JsonMemoryStore",
8
+ "RevisionConflict",
9
+ "TeamMemoryEntry",
10
+ "TeamMemoryStore",
11
+ ]
@@ -0,0 +1,52 @@
1
+ """Simple local JSON memory backend."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from pathlib import Path
8
+
9
+ from codecortex.core.contracts import MemoryStore
10
+
11
+
12
+ class JsonMemoryStore(MemoryStore):
13
+ def __init__(self, root: Path) -> None:
14
+ self.root = root
15
+ self.root.mkdir(parents=True, exist_ok=True)
16
+
17
+ def _path(self, namespace: str) -> Path:
18
+ safe = re.sub(r"[^a-zA-Z0-9_.-]+", "_", namespace)
19
+ return self.root / f"{safe}.json"
20
+
21
+ def _load(self, namespace: str) -> dict[str, str]:
22
+ path = self._path(namespace)
23
+ if not path.exists():
24
+ return {}
25
+ try:
26
+ data = json.loads(path.read_text(encoding="utf-8"))
27
+ except (json.JSONDecodeError, OSError):
28
+ return {}
29
+ return {str(key): str(value) for key, value in data.items()}
30
+
31
+ async def put(self, namespace: str, key: str, value: str) -> None:
32
+ data = self._load(namespace)
33
+ data[key] = value
34
+ path = self._path(namespace)
35
+ temp = path.with_suffix(".tmp")
36
+ temp.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
37
+ temp.replace(path)
38
+
39
+ async def get(self, namespace: str, key: str) -> str | None:
40
+ return self._load(namespace).get(key)
41
+
42
+ async def search(self, namespace: str, query: str, limit: int = 10) -> list[str]:
43
+ terms = {term.lower() for term in query.split() if term.strip()}
44
+ data = self._load(namespace)
45
+ scored: list[tuple[int, str]] = []
46
+ for key, value in data.items():
47
+ haystack = f"{key} {value}".lower()
48
+ score = sum(1 for term in terms if term in haystack)
49
+ if score:
50
+ scored.append((score, value))
51
+ scored.sort(key=lambda item: item[0], reverse=True)
52
+ return [value for _, value in scored[:limit]]