devtorch-core 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 (193) hide show
  1. devtorch_core/__init__.py +158 -0
  2. devtorch_core/aggphi_textual.py +275 -0
  3. devtorch_core/alerts/__init__.py +23 -0
  4. devtorch_core/alerts/base.py +46 -0
  5. devtorch_core/alerts/config.py +60 -0
  6. devtorch_core/alerts/dispatcher.py +110 -0
  7. devtorch_core/alerts/jira.py +96 -0
  8. devtorch_core/alerts/linear.py +72 -0
  9. devtorch_core/alerts/pagerduty.py +66 -0
  10. devtorch_core/alerts/slack.py +81 -0
  11. devtorch_core/alerts/teams.py +70 -0
  12. devtorch_core/audit/__init__.py +43 -0
  13. devtorch_core/audit/exporter.py +297 -0
  14. devtorch_core/audit/privacy.py +101 -0
  15. devtorch_core/audit/scrubber.py +149 -0
  16. devtorch_core/audit/service.py +67 -0
  17. devtorch_core/audit/signing.py +127 -0
  18. devtorch_core/broadcast/__init__.py +4 -0
  19. devtorch_core/broadcast/broadcaster.py +100 -0
  20. devtorch_core/broadcast/watcher.py +71 -0
  21. devtorch_core/capability.py +639 -0
  22. devtorch_core/cloud/__init__.py +1 -0
  23. devtorch_core/cloud/client_config.py +472 -0
  24. devtorch_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
  25. devtorch_core/cloud/client_configs/.claude-stdio.json +13 -0
  26. devtorch_core/cloud/client_configs/.cursor-mcp.json +13 -0
  27. devtorch_core/cloud/client_configs/.opencode-bridge.json +13 -0
  28. devtorch_core/cloud/client_configs/.opencode.json +15 -0
  29. devtorch_core/cloud/client_configs/.vscode-mcp.json +13 -0
  30. devtorch_core/cloud/devtorch-mcp-bridge.js +357 -0
  31. devtorch_core/cloud/mcp_client.py +229 -0
  32. devtorch_core/cloud/setup.py +144 -0
  33. devtorch_core/cloud/sync.py +143 -0
  34. devtorch_core/cloud/sync_bundle.py +603 -0
  35. devtorch_core/cloud/sync_conflicts.py +159 -0
  36. devtorch_core/cloud/sync_state.py +159 -0
  37. devtorch_core/cloud/team_sync.py +283 -0
  38. devtorch_core/codex/__init__.py +9 -0
  39. devtorch_core/codex/__main__.py +97 -0
  40. devtorch_core/codex/capture.py +208 -0
  41. devtorch_core/codex/proxy.py +412 -0
  42. devtorch_core/concept_catalog.py +209 -0
  43. devtorch_core/consolidation/__init__.py +3 -0
  44. devtorch_core/consolidation/synthesizer.py +87 -0
  45. devtorch_core/consolidation/workflow.py +175 -0
  46. devtorch_core/daemon/__init__.py +27 -0
  47. devtorch_core/daemon/supervisor.py +293 -0
  48. devtorch_core/daemon/watcher.py +244 -0
  49. devtorch_core/dashboard_api.py +2012 -0
  50. devtorch_core/deltaf.py +97 -0
  51. devtorch_core/disclosure.py +50 -0
  52. devtorch_core/divergence/__init__.py +3 -0
  53. devtorch_core/divergence/detector.py +166 -0
  54. devtorch_core/gateway/__init__.py +32 -0
  55. devtorch_core/gateway/key_manager.py +124 -0
  56. devtorch_core/gateway/metrics_webhook.py +252 -0
  57. devtorch_core/gateway/policy.py +262 -0
  58. devtorch_core/gateway/server.py +727 -0
  59. devtorch_core/gateway/sso.py +233 -0
  60. devtorch_core/gcc.py +1246 -0
  61. devtorch_core/github/__init__.py +35 -0
  62. devtorch_core/github/app.py +240 -0
  63. devtorch_core/github/comment_builder.py +113 -0
  64. devtorch_core/github/pat.py +76 -0
  65. devtorch_core/github/pr_parser.py +82 -0
  66. devtorch_core/github/pr_reporter.py +555 -0
  67. devtorch_core/gitlab/__init__.py +177 -0
  68. devtorch_core/hitl/__init__.py +4 -0
  69. devtorch_core/hitl/channels.py +129 -0
  70. devtorch_core/hitl/orchestrator.py +95 -0
  71. devtorch_core/hooks/__init__.py +17 -0
  72. devtorch_core/hooks/claude_code.py +228 -0
  73. devtorch_core/hooks/git_capture.py +341 -0
  74. devtorch_core/hooks/git_commit.py +182 -0
  75. devtorch_core/hooks/installer.py +733 -0
  76. devtorch_core/hooks/pre_commit.py +157 -0
  77. devtorch_core/hooks/runner.py +344 -0
  78. devtorch_core/identity/__init__.py +4 -0
  79. devtorch_core/identity/agent.py +86 -0
  80. devtorch_core/identity/providers.py +85 -0
  81. devtorch_core/invariants.py +182 -0
  82. devtorch_core/mcp/__init__.py +10 -0
  83. devtorch_core/mcp/auth.py +177 -0
  84. devtorch_core/mcp/server.py +1049 -0
  85. devtorch_core/metrics/__init__.py +35 -0
  86. devtorch_core/metrics/aggregate.py +215 -0
  87. devtorch_core/metrics/calibrate.py +198 -0
  88. devtorch_core/metrics/calibration.py +125 -0
  89. devtorch_core/metrics/credibility.py +288 -0
  90. devtorch_core/metrics/delivery_time.py +70 -0
  91. devtorch_core/metrics/dhs.py +126 -0
  92. devtorch_core/metrics/mcs.py +96 -0
  93. devtorch_core/metrics/roi.py +88 -0
  94. devtorch_core/metrics/session_writer.py +81 -0
  95. devtorch_core/metrics/shadow_ai.py +117 -0
  96. devtorch_core/metrics/sprint_writer.py +243 -0
  97. devtorch_core/observability/__init__.py +78 -0
  98. devtorch_core/observability/datadog.py +157 -0
  99. devtorch_core/observability/formatter.py +119 -0
  100. devtorch_core/observability/report.py +264 -0
  101. devtorch_core/observability/servicenow.py +147 -0
  102. devtorch_core/observability/splunk.py +218 -0
  103. devtorch_core/observability/webhook.py +227 -0
  104. devtorch_core/parser/__init__.py +30 -0
  105. devtorch_core/parser/blocks.py +216 -0
  106. devtorch_core/parser/inference.py +159 -0
  107. devtorch_core/parser/thinking.py +112 -0
  108. devtorch_core/projects.py +169 -0
  109. devtorch_core/prompt_artifact.py +76 -0
  110. devtorch_core/proxy/__init__.py +9 -0
  111. devtorch_core/proxy/routes/__init__.py +1 -0
  112. devtorch_core/proxy/routes/anthropic.py +264 -0
  113. devtorch_core/proxy/routes/azure_openai.py +336 -0
  114. devtorch_core/proxy/routes/gemini.py +331 -0
  115. devtorch_core/proxy/routes/groq.py +284 -0
  116. devtorch_core/proxy/routes/ollama.py +279 -0
  117. devtorch_core/proxy/routes/openai.py +287 -0
  118. devtorch_core/proxy/server.py +356 -0
  119. devtorch_core/query/__init__.py +15 -0
  120. devtorch_core/query/grep.py +181 -0
  121. devtorch_core/query/hybrid.py +86 -0
  122. devtorch_core/query/semantic.py +157 -0
  123. devtorch_core/rdp.py +105 -0
  124. devtorch_core/reasoning/__init__.py +4 -0
  125. devtorch_core/reasoning/entry.py +31 -0
  126. devtorch_core/reasoning/store.py +122 -0
  127. devtorch_core/reasoning_plus/__init__.py +70 -0
  128. devtorch_core/reasoning_plus/augmenter.py +326 -0
  129. devtorch_core/reasoning_plus/capture.py +51 -0
  130. devtorch_core/reasoning_plus/config.py +256 -0
  131. devtorch_core/reasoning_plus/context.py +262 -0
  132. devtorch_core/reasoning_plus/learning/__init__.py +72 -0
  133. devtorch_core/reasoning_plus/learning/analytics.py +141 -0
  134. devtorch_core/reasoning_plus/learning/api.py +313 -0
  135. devtorch_core/reasoning_plus/learning/chain.py +285 -0
  136. devtorch_core/reasoning_plus/learning/composer.py +74 -0
  137. devtorch_core/reasoning_plus/learning/cross_project.py +234 -0
  138. devtorch_core/reasoning_plus/learning/embeddings.py +209 -0
  139. devtorch_core/reasoning_plus/learning/extractor.py +207 -0
  140. devtorch_core/reasoning_plus/learning/models.py +116 -0
  141. devtorch_core/reasoning_plus/learning/provenance.py +126 -0
  142. devtorch_core/reasoning_plus/learning/recorder.py +81 -0
  143. devtorch_core/reasoning_plus/learning/relevance.py +122 -0
  144. devtorch_core/reasoning_plus/learning/state.py +86 -0
  145. devtorch_core/reasoning_plus/learning/store.py +160 -0
  146. devtorch_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
  147. devtorch_core/reasoning_plus/prompt.py +90 -0
  148. devtorch_core/rep.py +134 -0
  149. devtorch_core/rep_network/__init__.py +25 -0
  150. devtorch_core/rep_network/merge.py +70 -0
  151. devtorch_core/rep_network/node.py +137 -0
  152. devtorch_core/rep_network/server.py +140 -0
  153. devtorch_core/rep_network/sync.py +207 -0
  154. devtorch_core/sensitivity.py +182 -0
  155. devtorch_core/serve.py +258 -0
  156. devtorch_core/session/__init__.py +39 -0
  157. devtorch_core/session/disagreement.py +188 -0
  158. devtorch_core/session/models.py +114 -0
  159. devtorch_core/session/orchestrator.py +182 -0
  160. devtorch_core/session/planner.py +169 -0
  161. devtorch_core/session/simulator.py +132 -0
  162. devtorch_core/signing.py +290 -0
  163. devtorch_core/sis.py +197 -0
  164. devtorch_core/storage.py +308 -0
  165. devtorch_core/templates/__init__.py +6 -0
  166. devtorch_core/templates/engine.py +122 -0
  167. devtorch_core/templates/go.py +18 -0
  168. devtorch_core/templates/infra.py +19 -0
  169. devtorch_core/templates/library/__init__.py +18 -0
  170. devtorch_core/templates/library/api_design.md +27 -0
  171. devtorch_core/templates/library/bug_fix.md +27 -0
  172. devtorch_core/templates/library/decision_record.md +27 -0
  173. devtorch_core/templates/library/engine.py +228 -0
  174. devtorch_core/templates/library/security_review.md +30 -0
  175. devtorch_core/templates/python.py +19 -0
  176. devtorch_core/templates/react.py +18 -0
  177. devtorch_core/templates/typescript.py +18 -0
  178. devtorch_core/theta.py +221 -0
  179. devtorch_core/theta_synthesis.py +268 -0
  180. devtorch_core/topics.py +320 -0
  181. devtorch_core/variance.py +219 -0
  182. devtorch_core/wrapper/__init__.py +52 -0
  183. devtorch_core/wrapper/anthropic.py +487 -0
  184. devtorch_core/wrapper/base.py +562 -0
  185. devtorch_core/wrapper/bedrock.py +342 -0
  186. devtorch_core/wrapper/gemini.py +422 -0
  187. devtorch_core/wrapper/ollama.py +527 -0
  188. devtorch_core/wrapper/openai.py +461 -0
  189. devtorch_core-3.0.1.dist-info/METADATA +867 -0
  190. devtorch_core-3.0.1.dist-info/RECORD +193 -0
  191. devtorch_core-3.0.1.dist-info/WHEEL +5 -0
  192. devtorch_core-3.0.1.dist-info/entry_points.txt +2 -0
  193. devtorch_core-3.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,603 @@
1
+ """
2
+ devtorch_core.cloud.sync_bundle
3
+ ================================
4
+ Build, compress, chunk, merge, and apply sync bundles for bidirectional
5
+ sync between local .GCC/ and the MCP server.
6
+
7
+ A sync bundle is a JSON dict containing all 5 data types (commits, theta,
8
+ learnings, sensitivities, topics). Bundles are gzip-compressed and
9
+ base64-encoded for transport through JSON-RPC.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import base64
14
+ import gzip
15
+ import hashlib
16
+ import json
17
+ import logging
18
+ from collections import defaultdict
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ from devtorch_core.cloud.team_sync import _merge_learnings as _ts_merge_learnings
23
+
24
+ logger = logging.getLogger("devtorch.cloud.sync_bundle")
25
+
26
+ DEFAULT_CHUNK_SIZE = 500
27
+ _MAX_SYNCED_IDS_FOR_HASH = 10000
28
+
29
+
30
+ def compress(bundle: dict) -> str:
31
+ """Gzip + base64 compress a bundle for transport."""
32
+ raw = json.dumps(bundle, separators=(",", ":")).encode("utf-8")
33
+ compressed = gzip.compress(raw, compresslevel=6)
34
+ return base64.b64encode(compressed).decode("ascii")
35
+
36
+
37
+ def decompress(encoded: str) -> dict:
38
+ """Decompress a bundle from transport."""
39
+ compressed = base64.b64decode(encoded)
40
+ raw = gzip.decompress(compressed)
41
+ return json.loads(raw.decode("utf-8"))
42
+
43
+
44
+ def chunk(bundle: dict, chunk_size: int = DEFAULT_CHUNK_SIZE) -> list[dict]:
45
+ """Split a bundle into chunks of at most *chunk_size* items per data type."""
46
+ if chunk_size <= 0:
47
+ return [bundle]
48
+
49
+ chunks: list[dict] = []
50
+ for data_type, items in bundle.items():
51
+ if data_type in ("version", "synced_at"):
52
+ continue
53
+ if not isinstance(items, (list, dict)):
54
+ continue
55
+ if isinstance(items, list):
56
+ for i in range(0, len(items), chunk_size):
57
+ if len(chunks) <= i // chunk_size:
58
+ chunks.append({"version": "1"})
59
+ chunks[i // chunk_size][data_type] = items[i : i + chunk_size]
60
+ elif isinstance(items, dict):
61
+ if len(chunks) == 0:
62
+ chunks.append({"version": "1"})
63
+ chunks[0][data_type] = items
64
+
65
+ return chunks if chunks else [bundle]
66
+
67
+
68
+ def build_local_delta(gcc_dir: Path, remote_state: dict) -> dict:
69
+ """Build a delta bundle of items the server doesn't have (for push)."""
70
+ bundle: dict = {"version": "1"}
71
+ _add_commit_delta(gcc_dir, remote_state, bundle)
72
+ _add_theta_delta(gcc_dir, remote_state, bundle)
73
+ _add_learning_delta(gcc_dir, remote_state, bundle)
74
+ _add_sensitivity_delta(gcc_dir, remote_state, bundle)
75
+ _add_topic_delta(gcc_dir, remote_state, bundle)
76
+ return bundle
77
+
78
+
79
+ def build_remote_delta(gcc_dir: Path, client_state: dict) -> dict:
80
+ """Build a delta bundle of items the client doesn't have (for pull, server-side)."""
81
+ bundle: dict = {"version": "1"}
82
+ _add_pull_commit_delta(gcc_dir, client_state, bundle)
83
+ _add_pull_theta_delta(gcc_dir, client_state, bundle)
84
+ _add_pull_learning_delta(gcc_dir, client_state, bundle)
85
+ _add_pull_sensitivity_delta(gcc_dir, client_state, bundle)
86
+ _add_pull_topic_delta(gcc_dir, client_state, bundle)
87
+ return bundle
88
+
89
+
90
+ def build_server_state(gcc_dir: Path) -> dict:
91
+ """Return the server's current sync state (what it has)."""
92
+ return {
93
+ "commits": {"head": _get_max_commit_index(gcc_dir)},
94
+ "theta": _get_theta_per_concept_timestamps(gcc_dir),
95
+ "learnings": {"synced_ids": _get_learning_ids(gcc_dir), "count": _count_learnings(gcc_dir)},
96
+ "sensitivities": {"last_synced_ts": _get_latest_sensitivity_ts(gcc_dir)},
97
+ "topics": {"synced_custom_names": _get_custom_topic_names(gcc_dir)},
98
+ }
99
+
100
+
101
+ def merge_pull(gcc_dir: Path, remote_delta: dict, conflict_log: Any) -> dict:
102
+ """Merge a remote delta into local .GCC/. Returns counts + conflicts.
103
+
104
+ *conflict_log* is a ``ConflictLog`` instance (or duck-typed equivalent).
105
+ """
106
+ counts: dict[str, int] = {}
107
+ conflicts: list[dict] = []
108
+
109
+ if "commits" in remote_delta:
110
+ c, cf = _merge_commits(gcc_dir, remote_delta["commits"], conflict_log)
111
+ counts["commits"] = c
112
+ conflicts.extend(cf)
113
+
114
+ if "theta" in remote_delta:
115
+ c, cf = _merge_theta(gcc_dir, remote_delta["theta"], conflict_log)
116
+ counts["theta"] = c
117
+ conflicts.extend(cf)
118
+
119
+ if "learnings" in remote_delta:
120
+ c, cf = _merge_learnings(gcc_dir, remote_delta["learnings"], conflict_log)
121
+ counts["learnings"] = c
122
+ conflicts.extend(cf)
123
+
124
+ if "sensitivities" in remote_delta:
125
+ c, cf = _merge_sensitivities(gcc_dir, remote_delta["sensitivities"], conflict_log)
126
+ counts["sensitivities"] = c
127
+ conflicts.extend(cf)
128
+
129
+ if "topics" in remote_delta:
130
+ c, cf = _merge_topics(gcc_dir, remote_delta["topics"], conflict_log)
131
+ counts["topics"] = c
132
+ conflicts.extend(cf)
133
+
134
+ return {"counts": counts, "conflicts": conflicts}
135
+
136
+
137
+ def merge_push(gcc_dir: Path, local_delta: dict, conflict_log: Any) -> dict:
138
+ """Merge a local delta into server .GCC/. Returns counts + conflicts.
139
+
140
+ Same logic as merge_pull — the merge rules are symmetric.
141
+ """
142
+ return merge_pull(gcc_dir, local_delta, conflict_log)
143
+
144
+
145
+ def _merge_commits(gcc_dir: Path, remote_commits: list[dict], conflict_log: Any) -> tuple[int, list[dict]]:
146
+ """Add new commits (skip if ID exists). Log ID collisions."""
147
+ commits_dir = gcc_dir / "commits"
148
+ commits_dir.mkdir(parents=True, exist_ok=True)
149
+ added = 0
150
+ conflicts = []
151
+ for commit in remote_commits:
152
+ cid = commit.get("id")
153
+ if not cid:
154
+ continue
155
+ path = commits_dir / f"{cid}.json"
156
+ if path.exists():
157
+ try:
158
+ existing = json.loads(path.read_text(encoding="utf-8"))
159
+ if existing.get("message") != commit.get("message") or existing.get("concepts") != commit.get("concepts"):
160
+ conflict = _make_conflict("commit_id_collision", "high", "commits", cid,
161
+ {"message": existing.get("message"), "concepts": existing.get("concepts")},
162
+ {"message": commit.get("message"), "concepts": commit.get("concepts")},
163
+ "kept_local")
164
+ if conflict_log:
165
+ conflict["conflict_id"] = conflict_log.append(conflict)
166
+ conflicts.append(conflict)
167
+ except (json.JSONDecodeError, OSError):
168
+ pass
169
+ continue
170
+ try:
171
+ _atomic_write(path, json.dumps(commit, indent=2) + "\n")
172
+ added += 1
173
+ except OSError as exc:
174
+ logger.warning("devtorch: failed to write commit %s — %s", cid, exc)
175
+ return added, conflicts
176
+
177
+
178
+ def _merge_theta(gcc_dir: Path, remote_theta: dict, conflict_log: Any) -> tuple[int, list[dict]]:
179
+ """Merge remote theta using merge_theta rules. Log disclosure downgrades."""
180
+ from devtorch_core.theta import make_theta_store
181
+ theta_store = make_theta_store(gcc_dir)
182
+ theta = theta_store.load()
183
+ cv = theta.setdefault("coordination_vector", {})
184
+ conflicts = []
185
+
186
+ remote_cv = remote_theta.get("coordination_vector", {})
187
+ for concept, entry in remote_cv.items():
188
+ existing = cv.get(concept, {})
189
+ old_count = int(existing.get("event_count", 0))
190
+ new_count = int(entry.get("event_count", 0))
191
+ total = old_count + new_count
192
+
193
+ if total:
194
+ old_conf = float(existing.get("mean_confidence", entry["mean_confidence"]))
195
+ new_conf = float(entry["mean_confidence"])
196
+ blended = (old_conf * old_count + new_conf * new_count) / total
197
+ else:
198
+ blended = float(entry["mean_confidence"])
199
+
200
+ _LEVEL_RANK = {"PUBLIC": 0, "PROTECTED": 1, "PRIVATE": 2}
201
+ _RANK_LEVEL = {0: "PUBLIC", 1: "PROTECTED", 2: "PRIVATE"}
202
+ existing_rank = _LEVEL_RANK.get(existing.get("disclosure_level", "PUBLIC"), 0)
203
+ new_rank = _LEVEL_RANK.get(entry.get("disclosure_level", "PUBLIC"), 0)
204
+
205
+ if existing and existing_rank != new_rank and existing_rank > new_rank:
206
+ conflict = _make_conflict("theta_disclosure_downgrade", "low", "theta", concept,
207
+ {"disclosure_level": existing.get("disclosure_level")},
208
+ {"disclosure_level": entry.get("disclosure_level")},
209
+ "kept_max")
210
+ if conflict_log:
211
+ conflict["conflict_id"] = conflict_log.append(conflict)
212
+ conflicts.append(conflict)
213
+
214
+ merged_level = _RANK_LEVEL[max(existing_rank, new_rank)]
215
+ from datetime import datetime, timezone
216
+ cv[concept] = {
217
+ "mean_confidence": round(blended, 6),
218
+ "disclosure_level": merged_level,
219
+ "event_count": total,
220
+ "last_updated": datetime.now(timezone.utc).isoformat(),
221
+ }
222
+
223
+ theta_store.save(theta)
224
+ return len(remote_cv), conflicts
225
+
226
+
227
+ def _merge_learnings(gcc_dir: Path, remote_learnings: list[dict], conflict_log: Any) -> tuple[int, list[dict]]:
228
+ """Add new learnings. For existing IDs: max confidence, union concepts, most restrictive validity. Log content divergence."""
229
+ from devtorch_core.reasoning_plus.learning.models import Learning
230
+ from devtorch_core.reasoning_plus.learning.store import LearningStore
231
+ store = LearningStore(gcc_dir)
232
+ added = 0
233
+ conflicts = []
234
+ for rl in remote_learnings:
235
+ rid = rl.get("id")
236
+ if not rid:
237
+ continue
238
+ existing_learning = store.get(rid)
239
+ if existing_learning:
240
+ if existing_learning.content != rl.get("content", ""):
241
+ conflict = _make_conflict("learning_content_divergence", "medium", "learnings", rid,
242
+ {"content": existing_learning.content, "confidence": existing_learning.confidence},
243
+ {"content": rl.get("content"), "confidence": rl.get("confidence")},
244
+ "kept_local")
245
+ if conflict_log:
246
+ conflict["conflict_id"] = conflict_log.append(conflict)
247
+ conflicts.append(conflict)
248
+ existing_learning.confidence = max(existing_learning.confidence, float(rl.get("confidence", 0)))
249
+ existing_learning.trigger_concepts = list(set(existing_learning.trigger_concepts) | set(rl.get("trigger_concepts", [])))
250
+ valid_rank = {"active": 0, "stale": 1, "deprecated": 2}
251
+ if valid_rank.get(rl.get("validity", "active"), 0) > valid_rank.get(existing_learning.validity, 0):
252
+ existing_learning.validity = rl["validity"]
253
+ store.save(existing_learning, deduplicate=False)
254
+ else:
255
+ try:
256
+ learning = Learning.from_dict(rl)
257
+ store.save(learning, deduplicate=False)
258
+ added += 1
259
+ except Exception as exc:
260
+ logger.warning("devtorch: failed to save remote learning %s — %s", rid, exc)
261
+ return added, conflicts
262
+
263
+
264
+ def _merge_sensitivities(gcc_dir: Path, remote_events: list[dict], conflict_log: Any) -> tuple[int, list[dict]]:
265
+ """Append new sensitivity events (dedup by SHA-256 hash)."""
266
+ path = gcc_dir / "sensitivities" / "events.jsonl"
267
+ path.parent.mkdir(parents=True, exist_ok=path.parent != gcc_dir and True or True)
268
+ path.parent.mkdir(parents=True, exist_ok=True)
269
+
270
+ existing_hashes = set()
271
+ if path.exists():
272
+ try:
273
+ for line in path.read_text(encoding="utf-8").splitlines():
274
+ if line.strip():
275
+ existing_hashes.add(hashlib.sha256(line.encode("utf-8")).hexdigest())
276
+ except OSError:
277
+ pass
278
+
279
+ added = 0
280
+ with open(str(path), "a", encoding="utf-8") as f:
281
+ for event in remote_events:
282
+ line = json.dumps(event, separators=(",", ":"))
283
+ h = hashlib.sha256(line.encode("utf-8")).hexdigest()
284
+ if h in existing_hashes:
285
+ continue
286
+ existing_hashes.add(h)
287
+ f.write(line + "\n")
288
+ added += 1
289
+ return added, []
290
+
291
+
292
+ def _merge_topics(gcc_dir: Path, remote_topics: dict, conflict_log: Any) -> tuple[int, list[dict]]:
293
+ """Merge custom topics (union by name, union concepts). Log delete vs update."""
294
+ from devtorch_core.topics import TopicStore
295
+ store = TopicStore(gcc_dir)
296
+ remote_custom = remote_topics.get("custom_topics", {})
297
+ added = 0
298
+ conflicts = []
299
+
300
+ local_full = store._read_full()
301
+ local_custom = local_full.setdefault("custom_topics", {})
302
+
303
+ for name, concepts in remote_custom.items():
304
+ if name in local_custom:
305
+ if set(local_custom[name]) != set(concepts):
306
+ conflict = _make_conflict("topic_delete_vs_update", "medium", "topics", name,
307
+ {"concepts": local_custom[name]},
308
+ {"concepts": concepts},
309
+ "kept_updated")
310
+ if conflict_log:
311
+ conflict["conflict_id"] = conflict_log.append(conflict)
312
+ conflicts.append(conflict)
313
+ local_custom[name] = sorted(set(local_custom[name]) | set(concepts))
314
+ else:
315
+ local_custom[name] = sorted(set(concepts))
316
+ added += 1
317
+
318
+ store._write_full(local_full)
319
+ return added, conflicts
320
+
321
+
322
+ def _make_conflict(
323
+ type_: str, severity: str, data_type: str, item_id: str,
324
+ local_value: dict, remote_value: dict, resolution: str,
325
+ ) -> dict:
326
+ from datetime import datetime, timezone
327
+ return {
328
+ "type": type_,
329
+ "severity": severity,
330
+ "data_type": data_type,
331
+ "item_id": item_id,
332
+ "local_value": local_value,
333
+ "remote_value": remote_value,
334
+ "resolution": resolution,
335
+ "resolved": False,
336
+ "timestamp": datetime.now(timezone.utc).isoformat(),
337
+ }
338
+
339
+
340
+ def _add_commit_delta(gcc_dir: Path, remote_state: dict, bundle: dict) -> None:
341
+ commits_dir = gcc_dir / "commits"
342
+ if not commits_dir.exists():
343
+ return
344
+ remote_commits = remote_state.get("commits", {})
345
+ # Server state exposes commit progress via "head" (max commit id). Fall back
346
+ # to synced_ids for compatibility with older server states.
347
+ remote_head = remote_commits.get("head")
348
+ remote_synced = set(remote_commits.get("synced_ids", []))
349
+ delta = []
350
+ for path in sorted(commits_dir.glob("*.json")):
351
+ try:
352
+ data = json.loads(path.read_text(encoding="utf-8"))
353
+ cid = data.get("id")
354
+ if remote_head is not None and cid is not None:
355
+ if cid > remote_head:
356
+ delta.append(data)
357
+ elif cid not in remote_synced:
358
+ delta.append(data)
359
+ except (json.JSONDecodeError, OSError):
360
+ continue
361
+ if delta:
362
+ bundle["commits"] = delta
363
+
364
+
365
+ def _add_theta_delta(gcc_dir: Path, remote_state: dict, bundle: dict) -> None:
366
+ path = gcc_dir / "theta.json"
367
+ if not path.exists():
368
+ return
369
+ try:
370
+ theta = json.loads(path.read_text(encoding="utf-8"))
371
+ except (json.JSONDecodeError, OSError):
372
+ return
373
+ cv = theta.get("coordination_vector", {})
374
+ remote_concepts = remote_state.get("theta", {})
375
+ delta_cv = {}
376
+ for concept, entry in cv.items():
377
+ remote_ts = remote_concepts.get(concept, {}).get("last_updated", "")
378
+ local_ts = entry.get("last_updated", "")
379
+ if local_ts > remote_ts:
380
+ delta_cv[concept] = entry
381
+ if delta_cv:
382
+ bundle["theta"] = {"version": 1, "coordination_vector": delta_cv}
383
+
384
+
385
+ def _add_learning_delta(gcc_dir: Path, remote_state: dict, bundle: dict) -> None:
386
+ learn_dir = gcc_dir / "reasoning_learnings" / "learnings"
387
+ if not learn_dir.exists():
388
+ return
389
+ remote_ids = set(remote_state.get("learnings", {}).get("synced_ids", []))
390
+ delta = []
391
+ for path in learn_dir.glob("*.json"):
392
+ try:
393
+ data = json.loads(path.read_text(encoding="utf-8"))
394
+ if data.get("id") not in remote_ids:
395
+ delta.append(data)
396
+ except (json.JSONDecodeError, OSError):
397
+ continue
398
+ if delta:
399
+ bundle["learnings"] = delta
400
+
401
+
402
+ def _add_sensitivity_delta(gcc_dir: Path, remote_state: dict, bundle: dict) -> None:
403
+ path = gcc_dir / "sensitivities" / "events.jsonl"
404
+ if not path.exists():
405
+ return
406
+ remote_ts = remote_state.get("sensitivities", {}).get("last_synced_ts", "")
407
+ delta = []
408
+ try:
409
+ for line in path.read_text(encoding="utf-8").splitlines():
410
+ if not line.strip():
411
+ continue
412
+ ev = json.loads(line)
413
+ payload = ev if "target_concept" in ev else ev.get("payload", {})
414
+ created = payload.get("created_at", "") or payload.get("timestamp", "")
415
+ if created > remote_ts:
416
+ delta.append(payload)
417
+ except (OSError, json.JSONDecodeError):
418
+ pass
419
+ if delta:
420
+ bundle["sensitivities"] = delta
421
+
422
+
423
+ def _add_topic_delta(gcc_dir: Path, remote_state: dict, bundle: dict) -> None:
424
+ from devtorch_core.topics import TopicStore
425
+ store = TopicStore(gcc_dir)
426
+ remote_names = set(remote_state.get("topics", {}).get("synced_custom_names", []))
427
+ local_custom = store._read_custom()
428
+ delta = {name: concepts for name, concepts in local_custom.items() if name not in remote_names}
429
+ if delta:
430
+ bundle["topics"] = {"custom_topics": delta}
431
+
432
+
433
+ def _add_pull_commit_delta(gcc_dir: Path, client_state: dict, bundle: dict) -> None:
434
+ commits_dir = gcc_dir / "commits"
435
+ if not commits_dir.exists():
436
+ return
437
+ client_commits = client_state.get("commits", {})
438
+ client_head = client_commits.get("remote_head")
439
+ client_ids = set(client_commits.get("synced_ids", []))
440
+ delta = []
441
+ for path in sorted(commits_dir.glob("*.json")):
442
+ try:
443
+ data = json.loads(path.read_text(encoding="utf-8"))
444
+ cid = data.get("id")
445
+ if client_head is not None and cid is not None:
446
+ if cid > client_head:
447
+ delta.append(data)
448
+ elif cid not in client_ids:
449
+ delta.append(data)
450
+ except (json.JSONDecodeError, OSError):
451
+ continue
452
+ if delta:
453
+ bundle["commits"] = delta
454
+
455
+
456
+ def _add_pull_theta_delta(gcc_dir: Path, client_state: dict, bundle: dict) -> None:
457
+ path = gcc_dir / "theta.json"
458
+ if not path.exists():
459
+ return
460
+ try:
461
+ theta = json.loads(path.read_text(encoding="utf-8"))
462
+ except (json.JSONDecodeError, OSError):
463
+ return
464
+ cv = theta.get("coordination_vector", {})
465
+ client_concepts = client_state.get("theta", {})
466
+ delta_cv = {}
467
+ for concept, entry in cv.items():
468
+ client_ts = client_concepts.get(concept, {}).get("last_updated", "")
469
+ local_ts = entry.get("last_updated", "")
470
+ if local_ts > client_ts:
471
+ delta_cv[concept] = entry
472
+ if delta_cv:
473
+ bundle["theta"] = {"version": 1, "coordination_vector": delta_cv}
474
+
475
+
476
+ def _add_pull_learning_delta(gcc_dir: Path, client_state: dict, bundle: dict) -> None:
477
+ learn_dir = gcc_dir / "reasoning_learnings" / "learnings"
478
+ if not learn_dir.exists():
479
+ return
480
+ client_ids = set(client_state.get("learnings", {}).get("synced_ids", []))
481
+ delta = []
482
+ for path in learn_dir.glob("*.json"):
483
+ try:
484
+ data = json.loads(path.read_text(encoding="utf-8"))
485
+ if data.get("id") not in client_ids:
486
+ delta.append(data)
487
+ except (json.JSONDecodeError, OSError):
488
+ continue
489
+ if delta:
490
+ bundle["learnings"] = delta
491
+
492
+
493
+ def _add_pull_sensitivity_delta(gcc_dir: Path, client_state: dict, bundle: dict) -> None:
494
+ path = gcc_dir / "sensitivities" / "events.jsonl"
495
+ if not path.exists():
496
+ return
497
+ client_ts = client_state.get("sensitivities", {}).get("last_synced_ts", "")
498
+ delta = []
499
+ try:
500
+ for line in path.read_text(encoding="utf-8").splitlines():
501
+ if not line.strip():
502
+ continue
503
+ ev = json.loads(line)
504
+ payload = ev if "target_concept" in ev else ev.get("payload", {})
505
+ created = payload.get("created_at", "") or payload.get("timestamp", "")
506
+ if created > client_ts:
507
+ delta.append(payload)
508
+ except (OSError, json.JSONDecodeError):
509
+ pass
510
+ if delta:
511
+ bundle["sensitivities"] = delta
512
+
513
+
514
+ def _add_pull_topic_delta(gcc_dir: Path, client_state: dict, bundle: dict) -> None:
515
+ from devtorch_core.topics import TopicStore
516
+ store = TopicStore(gcc_dir)
517
+ client_names = set(client_state.get("topics", {}).get("synced_custom_names", []))
518
+ local_custom = store._read_custom()
519
+ delta = {name: concepts for name, concepts in local_custom.items() if name not in client_names}
520
+ if delta:
521
+ bundle["topics"] = {"custom_topics": delta}
522
+
523
+
524
+ def _get_max_commit_index(gcc_dir: Path) -> str:
525
+ commits_dir = gcc_dir / "commits"
526
+ if not commits_dir.exists():
527
+ return "0"
528
+ max_id = "0"
529
+ for path in commits_dir.glob("*.json"):
530
+ stem = path.stem
531
+ if stem > max_id:
532
+ max_id = stem
533
+ return max_id
534
+
535
+
536
+ def _get_theta_per_concept_timestamps(gcc_dir: Path) -> dict[str, dict[str, str]]:
537
+ path = gcc_dir / "theta.json"
538
+ if not path.exists():
539
+ return {}
540
+ try:
541
+ theta = json.loads(path.read_text(encoding="utf-8"))
542
+ except (json.JSONDecodeError, OSError):
543
+ return {}
544
+ cv = theta.get("coordination_vector", {})
545
+ return {c: {"last_updated": e.get("last_updated", "")} for c, e in cv.items()}
546
+
547
+
548
+ def _get_learning_ids(gcc_dir: Path) -> list[str]:
549
+ learn_dir = gcc_dir / "reasoning_learnings" / "learnings"
550
+ if not learn_dir.exists():
551
+ return []
552
+ ids = []
553
+ for path in learn_dir.glob("*.json"):
554
+ try:
555
+ data = json.loads(path.read_text(encoding="utf-8"))
556
+ if data.get("id"):
557
+ ids.append(data["id"])
558
+ except (json.JSONDecodeError, OSError):
559
+ continue
560
+ return ids
561
+
562
+
563
+ def _count_learnings(gcc_dir: Path) -> int:
564
+ learn_dir = gcc_dir / "reasoning_learnings" / "learnings"
565
+ if not learn_dir.exists():
566
+ return 0
567
+ return len(list(learn_dir.glob("*.json")))
568
+
569
+
570
+ def _get_latest_sensitivity_ts(gcc_dir: Path) -> str:
571
+ path = gcc_dir / "sensitivities" / "events.jsonl"
572
+ if not path.exists():
573
+ return ""
574
+ latest = ""
575
+ try:
576
+ for line in path.read_text(encoding="utf-8").splitlines():
577
+ if not line.strip():
578
+ continue
579
+ ev = json.loads(line)
580
+ ts = ev.get("created_at", "") or ev.get("timestamp", "")
581
+ if ts > latest:
582
+ latest = ts
583
+ except (OSError, json.JSONDecodeError):
584
+ pass
585
+ return latest
586
+
587
+
588
+ def _get_custom_topic_names(gcc_dir: Path) -> list[str]:
589
+ from devtorch_core.topics import TopicStore
590
+ return list(TopicStore(gcc_dir)._read_custom().keys())
591
+
592
+
593
+ def _atomic_write(path: Path, content: str) -> None:
594
+ tmp = path.with_suffix(path.suffix + ".tmp")
595
+ try:
596
+ tmp.write_text(content, encoding="utf-8")
597
+ tmp.replace(path)
598
+ finally:
599
+ if tmp.exists():
600
+ try:
601
+ tmp.unlink()
602
+ except Exception:
603
+ pass