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.
- devtorch_core/__init__.py +158 -0
- devtorch_core/aggphi_textual.py +275 -0
- devtorch_core/alerts/__init__.py +23 -0
- devtorch_core/alerts/base.py +46 -0
- devtorch_core/alerts/config.py +60 -0
- devtorch_core/alerts/dispatcher.py +110 -0
- devtorch_core/alerts/jira.py +96 -0
- devtorch_core/alerts/linear.py +72 -0
- devtorch_core/alerts/pagerduty.py +66 -0
- devtorch_core/alerts/slack.py +81 -0
- devtorch_core/alerts/teams.py +70 -0
- devtorch_core/audit/__init__.py +43 -0
- devtorch_core/audit/exporter.py +297 -0
- devtorch_core/audit/privacy.py +101 -0
- devtorch_core/audit/scrubber.py +149 -0
- devtorch_core/audit/service.py +67 -0
- devtorch_core/audit/signing.py +127 -0
- devtorch_core/broadcast/__init__.py +4 -0
- devtorch_core/broadcast/broadcaster.py +100 -0
- devtorch_core/broadcast/watcher.py +71 -0
- devtorch_core/capability.py +639 -0
- devtorch_core/cloud/__init__.py +1 -0
- devtorch_core/cloud/client_config.py +472 -0
- devtorch_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
- devtorch_core/cloud/client_configs/.claude-stdio.json +13 -0
- devtorch_core/cloud/client_configs/.cursor-mcp.json +13 -0
- devtorch_core/cloud/client_configs/.opencode-bridge.json +13 -0
- devtorch_core/cloud/client_configs/.opencode.json +15 -0
- devtorch_core/cloud/client_configs/.vscode-mcp.json +13 -0
- devtorch_core/cloud/devtorch-mcp-bridge.js +357 -0
- devtorch_core/cloud/mcp_client.py +229 -0
- devtorch_core/cloud/setup.py +144 -0
- devtorch_core/cloud/sync.py +143 -0
- devtorch_core/cloud/sync_bundle.py +603 -0
- devtorch_core/cloud/sync_conflicts.py +159 -0
- devtorch_core/cloud/sync_state.py +159 -0
- devtorch_core/cloud/team_sync.py +283 -0
- devtorch_core/codex/__init__.py +9 -0
- devtorch_core/codex/__main__.py +97 -0
- devtorch_core/codex/capture.py +208 -0
- devtorch_core/codex/proxy.py +412 -0
- devtorch_core/concept_catalog.py +209 -0
- devtorch_core/consolidation/__init__.py +3 -0
- devtorch_core/consolidation/synthesizer.py +87 -0
- devtorch_core/consolidation/workflow.py +175 -0
- devtorch_core/daemon/__init__.py +27 -0
- devtorch_core/daemon/supervisor.py +293 -0
- devtorch_core/daemon/watcher.py +244 -0
- devtorch_core/dashboard_api.py +2012 -0
- devtorch_core/deltaf.py +97 -0
- devtorch_core/disclosure.py +50 -0
- devtorch_core/divergence/__init__.py +3 -0
- devtorch_core/divergence/detector.py +166 -0
- devtorch_core/gateway/__init__.py +32 -0
- devtorch_core/gateway/key_manager.py +124 -0
- devtorch_core/gateway/metrics_webhook.py +252 -0
- devtorch_core/gateway/policy.py +262 -0
- devtorch_core/gateway/server.py +727 -0
- devtorch_core/gateway/sso.py +233 -0
- devtorch_core/gcc.py +1246 -0
- devtorch_core/github/__init__.py +35 -0
- devtorch_core/github/app.py +240 -0
- devtorch_core/github/comment_builder.py +113 -0
- devtorch_core/github/pat.py +76 -0
- devtorch_core/github/pr_parser.py +82 -0
- devtorch_core/github/pr_reporter.py +555 -0
- devtorch_core/gitlab/__init__.py +177 -0
- devtorch_core/hitl/__init__.py +4 -0
- devtorch_core/hitl/channels.py +129 -0
- devtorch_core/hitl/orchestrator.py +95 -0
- devtorch_core/hooks/__init__.py +17 -0
- devtorch_core/hooks/claude_code.py +228 -0
- devtorch_core/hooks/git_capture.py +341 -0
- devtorch_core/hooks/git_commit.py +182 -0
- devtorch_core/hooks/installer.py +733 -0
- devtorch_core/hooks/pre_commit.py +157 -0
- devtorch_core/hooks/runner.py +344 -0
- devtorch_core/identity/__init__.py +4 -0
- devtorch_core/identity/agent.py +86 -0
- devtorch_core/identity/providers.py +85 -0
- devtorch_core/invariants.py +182 -0
- devtorch_core/mcp/__init__.py +10 -0
- devtorch_core/mcp/auth.py +177 -0
- devtorch_core/mcp/server.py +1049 -0
- devtorch_core/metrics/__init__.py +35 -0
- devtorch_core/metrics/aggregate.py +215 -0
- devtorch_core/metrics/calibrate.py +198 -0
- devtorch_core/metrics/calibration.py +125 -0
- devtorch_core/metrics/credibility.py +288 -0
- devtorch_core/metrics/delivery_time.py +70 -0
- devtorch_core/metrics/dhs.py +126 -0
- devtorch_core/metrics/mcs.py +96 -0
- devtorch_core/metrics/roi.py +88 -0
- devtorch_core/metrics/session_writer.py +81 -0
- devtorch_core/metrics/shadow_ai.py +117 -0
- devtorch_core/metrics/sprint_writer.py +243 -0
- devtorch_core/observability/__init__.py +78 -0
- devtorch_core/observability/datadog.py +157 -0
- devtorch_core/observability/formatter.py +119 -0
- devtorch_core/observability/report.py +264 -0
- devtorch_core/observability/servicenow.py +147 -0
- devtorch_core/observability/splunk.py +218 -0
- devtorch_core/observability/webhook.py +227 -0
- devtorch_core/parser/__init__.py +30 -0
- devtorch_core/parser/blocks.py +216 -0
- devtorch_core/parser/inference.py +159 -0
- devtorch_core/parser/thinking.py +112 -0
- devtorch_core/projects.py +169 -0
- devtorch_core/prompt_artifact.py +76 -0
- devtorch_core/proxy/__init__.py +9 -0
- devtorch_core/proxy/routes/__init__.py +1 -0
- devtorch_core/proxy/routes/anthropic.py +264 -0
- devtorch_core/proxy/routes/azure_openai.py +336 -0
- devtorch_core/proxy/routes/gemini.py +331 -0
- devtorch_core/proxy/routes/groq.py +284 -0
- devtorch_core/proxy/routes/ollama.py +279 -0
- devtorch_core/proxy/routes/openai.py +287 -0
- devtorch_core/proxy/server.py +356 -0
- devtorch_core/query/__init__.py +15 -0
- devtorch_core/query/grep.py +181 -0
- devtorch_core/query/hybrid.py +86 -0
- devtorch_core/query/semantic.py +157 -0
- devtorch_core/rdp.py +105 -0
- devtorch_core/reasoning/__init__.py +4 -0
- devtorch_core/reasoning/entry.py +31 -0
- devtorch_core/reasoning/store.py +122 -0
- devtorch_core/reasoning_plus/__init__.py +70 -0
- devtorch_core/reasoning_plus/augmenter.py +326 -0
- devtorch_core/reasoning_plus/capture.py +51 -0
- devtorch_core/reasoning_plus/config.py +256 -0
- devtorch_core/reasoning_plus/context.py +262 -0
- devtorch_core/reasoning_plus/learning/__init__.py +72 -0
- devtorch_core/reasoning_plus/learning/analytics.py +141 -0
- devtorch_core/reasoning_plus/learning/api.py +313 -0
- devtorch_core/reasoning_plus/learning/chain.py +285 -0
- devtorch_core/reasoning_plus/learning/composer.py +74 -0
- devtorch_core/reasoning_plus/learning/cross_project.py +234 -0
- devtorch_core/reasoning_plus/learning/embeddings.py +209 -0
- devtorch_core/reasoning_plus/learning/extractor.py +207 -0
- devtorch_core/reasoning_plus/learning/models.py +116 -0
- devtorch_core/reasoning_plus/learning/provenance.py +126 -0
- devtorch_core/reasoning_plus/learning/recorder.py +81 -0
- devtorch_core/reasoning_plus/learning/relevance.py +122 -0
- devtorch_core/reasoning_plus/learning/state.py +86 -0
- devtorch_core/reasoning_plus/learning/store.py +160 -0
- devtorch_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
- devtorch_core/reasoning_plus/prompt.py +90 -0
- devtorch_core/rep.py +134 -0
- devtorch_core/rep_network/__init__.py +25 -0
- devtorch_core/rep_network/merge.py +70 -0
- devtorch_core/rep_network/node.py +137 -0
- devtorch_core/rep_network/server.py +140 -0
- devtorch_core/rep_network/sync.py +207 -0
- devtorch_core/sensitivity.py +182 -0
- devtorch_core/serve.py +258 -0
- devtorch_core/session/__init__.py +39 -0
- devtorch_core/session/disagreement.py +188 -0
- devtorch_core/session/models.py +114 -0
- devtorch_core/session/orchestrator.py +182 -0
- devtorch_core/session/planner.py +169 -0
- devtorch_core/session/simulator.py +132 -0
- devtorch_core/signing.py +290 -0
- devtorch_core/sis.py +197 -0
- devtorch_core/storage.py +308 -0
- devtorch_core/templates/__init__.py +6 -0
- devtorch_core/templates/engine.py +122 -0
- devtorch_core/templates/go.py +18 -0
- devtorch_core/templates/infra.py +19 -0
- devtorch_core/templates/library/__init__.py +18 -0
- devtorch_core/templates/library/api_design.md +27 -0
- devtorch_core/templates/library/bug_fix.md +27 -0
- devtorch_core/templates/library/decision_record.md +27 -0
- devtorch_core/templates/library/engine.py +228 -0
- devtorch_core/templates/library/security_review.md +30 -0
- devtorch_core/templates/python.py +19 -0
- devtorch_core/templates/react.py +18 -0
- devtorch_core/templates/typescript.py +18 -0
- devtorch_core/theta.py +221 -0
- devtorch_core/theta_synthesis.py +268 -0
- devtorch_core/topics.py +320 -0
- devtorch_core/variance.py +219 -0
- devtorch_core/wrapper/__init__.py +52 -0
- devtorch_core/wrapper/anthropic.py +487 -0
- devtorch_core/wrapper/base.py +562 -0
- devtorch_core/wrapper/bedrock.py +342 -0
- devtorch_core/wrapper/gemini.py +422 -0
- devtorch_core/wrapper/ollama.py +527 -0
- devtorch_core/wrapper/openai.py +461 -0
- devtorch_core-3.0.1.dist-info/METADATA +867 -0
- devtorch_core-3.0.1.dist-info/RECORD +193 -0
- devtorch_core-3.0.1.dist-info/WHEEL +5 -0
- devtorch_core-3.0.1.dist-info/entry_points.txt +2 -0
- 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
|