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,94 @@
1
+ """
2
+ devtorch_core.reasoning_plus.learning.theta_learning_bridge
3
+ ============================================================
4
+ Bridge between DRPL learning outcomes and the coordination vector Θ.
5
+
6
+ Feeds per-concept success rates into ThetaStore.ripple() so that
7
+ learning outcomes influence the shared coordination vector.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from devtorch_core.reasoning_plus.learning.store import LearningStore
16
+ from devtorch_core.theta import make_theta_store
17
+
18
+ logger = logging.getLogger("devtorch.reasoning_plus.learning")
19
+
20
+
21
+ def sync_learnings_to_theta(
22
+ gcc_dir: Path | str,
23
+ min_samples: int = 3,
24
+ disclosure: str = "PUBLIC",
25
+ ) -> dict[str, Any]:
26
+ """Compute per-concept success rates from learnings and feed them into Θ.
27
+
28
+ For each concept with at least *min_samples* learnings, creates a
29
+ sensitivity event with confidence = success_rate and feeds it through
30
+ ThetaStore.ripple(). This means concepts with high success rates
31
+ produce high-confidence events, and failing concepts produce low-confidence
32
+ signals in the coordination vector.
33
+
34
+ Args:
35
+ gcc_dir: Path to the local .GCC/ directory.
36
+ min_samples: Minimum learnings per concept to include.
37
+ disclosure: Disclosure level for generated events.
38
+
39
+ Returns:
40
+ A dict with keys:
41
+ - concepts_synced: number of concepts fed into theta
42
+ - events_generated: total events generated
43
+ - theta_concepts_after: concept count in theta after sync
44
+ """
45
+ from devtorch_core.reasoning_plus.learning.analytics import concept_stats
46
+
47
+ store = LearningStore(gcc_dir)
48
+ stats = concept_stats(store)
49
+
50
+ events: list[dict] = []
51
+ concepts_synced = 0
52
+
53
+ for cname, data in stats.items():
54
+ if cname == "__untyped__":
55
+ continue
56
+ if data["total"] < min_samples:
57
+ continue
58
+ events.append({
59
+ "target_concept": cname,
60
+ "confidence": data["success_rate"],
61
+ "disclosure_level": disclosure,
62
+ "created_at": _now_iso(),
63
+ "source": "learning-analytics",
64
+ })
65
+ concepts_synced += 1
66
+
67
+ if not events:
68
+ return {
69
+ "concepts_synced": 0,
70
+ "events_generated": 0,
71
+ "theta_concepts_after": len(
72
+ make_theta_store(Path(gcc_dir)).load().get("coordination_vector", {})
73
+ ),
74
+ }
75
+
76
+ theta_store = make_theta_store(Path(gcc_dir))
77
+ theta_store.ripple(events)
78
+ updated = theta_store.load()
79
+ theta_count = len(updated.get("coordination_vector", {}))
80
+
81
+ logger.info(
82
+ "devtorch: synced %d concept(s) into Θ (%d events) — theta now has %d concept(s)",
83
+ concepts_synced, len(events), theta_count,
84
+ )
85
+ return {
86
+ "concepts_synced": concepts_synced,
87
+ "events_generated": len(events),
88
+ "theta_concepts_after": theta_count,
89
+ }
90
+
91
+
92
+ def _now_iso() -> str:
93
+ from datetime import datetime, timezone
94
+ return datetime.now(timezone.utc).isoformat()
@@ -0,0 +1,90 @@
1
+ """
2
+ devtorch_core.reasoning_plus.prompt
3
+ ===================================
4
+ Reasoning Plus prompt templates.
5
+
6
+ The default template is task-agnostic: it asks the model to reason step-by-step
7
+ inside <thinking> tags before producing the final answer. The SWE-Bench runner
8
+ uses a patch-specific override via the `task_prompt` parameter.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ DEFAULT_REASONING_DIRECTIVE = """You are a careful reasoning assistant.
13
+
14
+ Before giving your final answer, think step by step inside <thinking>...</thinking> tags.
15
+ Explain your reasoning, the relevant facts, and any trade-offs you considered.
16
+ Then provide the final answer outside the <thinking> block.
17
+
18
+ You MUST output the <thinking> block before the final answer.
19
+ """
20
+
21
+
22
+ PATCH_REASONING_DIRECTIVE = """You are an expert software engineer fixing a GitHub issue in a Python repository.
23
+
24
+ Your job is to produce a single, correct patch in unified diff format that the repository maintainers could apply with `git apply`.
25
+
26
+ Rules:
27
+ - Edit only the files needed to fix the issue.
28
+ - Use exact `git diff` style hunks: `--- a/<path>` and `+++ b/<path>` headers, `@@ -start,len +start,len @@` context lines.
29
+ - Context lines must match the original file exactly (indentation, spacing, content).
30
+ - Do not add line numbers, explanations, or markdown inside the patch.
31
+ - Do not output any text after `</patch>`.
32
+
33
+ Before writing the patch, you MUST think step by step inside <thinking>...</thinking> tags.
34
+ The thinking block should explain your analysis of the issue, the files that need to change, and the fix strategy.
35
+ Then write the patch between <patch>...</patch> tags.
36
+
37
+ You MUST output the <thinking> block before the <patch> block.
38
+
39
+ Example of the required output format:
40
+
41
+ <thinking>
42
+ 1. Root cause: the function X does not handle Y because Z.
43
+ 2. Files to edit: src/example.py
44
+ 3. Fix strategy: add a guard before the call to X.
45
+ </thinking>
46
+
47
+ <patch>
48
+ --- a/src/example.py
49
+ +++ b/src/example.py
50
+ @@ -10,7 +10,7 @@
51
+ def old_function():
52
+ x = 1
53
+ - return x
54
+ + return x + 1
55
+
56
+ def other_function():
57
+ pass
58
+ </patch>
59
+ """
60
+
61
+
62
+ def build_reasoning_plus_system_prompt(
63
+ base_prompt: str,
64
+ smart_context: str = "",
65
+ *,
66
+ require_thinking: bool = True,
67
+ task_prompt: str = "",
68
+ ) -> str:
69
+ """
70
+ Build a Reasoning Plus system prompt.
71
+
72
+ Args:
73
+ base_prompt: the caller's existing system prompt.
74
+ smart_context: optional markdown snippet of related files.
75
+ require_thinking: whether to append the thinking directive.
76
+ task_prompt: optional task-specific directive (e.g. the SWE-Bench patch prompt).
77
+ If empty, the generic task-agnostic directive is used.
78
+ """
79
+ parts = [base_prompt.strip()]
80
+
81
+ if smart_context:
82
+ parts.append("[DevTorch smart context]")
83
+ parts.append(smart_context.strip())
84
+
85
+ if require_thinking:
86
+ directive = task_prompt.strip() if task_prompt else DEFAULT_REASONING_DIRECTIVE.strip()
87
+ parts.append("[DevTorch reasoning directive]")
88
+ parts.append(directive)
89
+
90
+ return "\n\n".join(parts)
devtorch_core/rep.py ADDED
@@ -0,0 +1,134 @@
1
+ """
2
+ Sprint 5 – REP (Reasoning Exchange Protocol) envelope schema and local transport.
3
+
4
+ Message envelope: agent_id, round, trust, payload hashes. Optional signatures.
5
+ Local transport: file-based ledger with deterministic ordering for record/replay.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import json
12
+ from dataclasses import dataclass, field
13
+ from pathlib import Path
14
+ from typing import Any, Dict, List, Optional
15
+
16
+ REP_VERSION = "rep-v1"
17
+ REP_LEDGER_NAME = "rep_ledger.jsonl"
18
+ REP_DIR_NAME = "rep"
19
+
20
+
21
+ def _hash_payload(payload: Any) -> str:
22
+ """SHA-256 hex digest of JSON-serialized payload."""
23
+ return hashlib.sha256(json.dumps(payload, sort_keys=True).encode("utf-8")).hexdigest()
24
+
25
+
26
+ @dataclass
27
+ class REPEnvelope:
28
+ """REP message envelope for sensitivity exchange."""
29
+ agent_id: str
30
+ round_id: int
31
+ trust: float # [0, 1] optional trust score
32
+ payload_hash: str
33
+ payload_type: str = "sensitivity" # sensitivity | control | ack
34
+ payload_ref: Optional[str] = None # optional pointer to full payload
35
+ signature: Optional[str] = None # optional crypto signature
36
+
37
+ def to_dict(self) -> dict:
38
+ d = {
39
+ "version": REP_VERSION,
40
+ "agent_id": self.agent_id,
41
+ "round_id": self.round_id,
42
+ "trust": self.trust,
43
+ "payload_hash": self.payload_hash,
44
+ "payload_type": self.payload_type,
45
+ }
46
+ if self.payload_ref is not None:
47
+ d["payload_ref"] = self.payload_ref
48
+ if self.signature is not None:
49
+ d["signature"] = self.signature
50
+ return d
51
+
52
+ @classmethod
53
+ def from_dict(cls, d: dict) -> "REPEnvelope":
54
+ return cls(
55
+ agent_id=str(d["agent_id"]),
56
+ round_id=int(d["round_id"]),
57
+ trust=float(d.get("trust", 0.5)),
58
+ payload_hash=str(d["payload_hash"]),
59
+ payload_type=str(d.get("payload_type", "sensitivity")),
60
+ payload_ref=d.get("payload_ref"),
61
+ signature=d.get("signature"),
62
+ )
63
+
64
+
65
+ class REPLedger:
66
+ """
67
+ Append-only REP ledger for local transport. Deterministic ordering by
68
+ sequence id and timestamp for record/replay.
69
+ """
70
+ def __init__(self, gcc_dir: Path) -> None:
71
+ self.rep_dir = gcc_dir / REP_DIR_NAME
72
+ self.ledger_path = self.rep_dir / REP_LEDGER_NAME
73
+
74
+ def ensure_dir(self) -> None:
75
+ self.rep_dir.mkdir(parents=True, exist_ok=True)
76
+
77
+ def append(self, envelope: REPEnvelope, timestamp: str, sequence_id: Optional[int] = None) -> int:
78
+ """Append envelope to ledger; return sequence id (1-based)."""
79
+ self.ensure_dir()
80
+ if sequence_id is None:
81
+ sequence_id = self._next_sequence_id()
82
+ record = {
83
+ "sequence_id": sequence_id,
84
+ "timestamp": timestamp,
85
+ "envelope": envelope.to_dict(),
86
+ }
87
+ with self.ledger_path.open("a", encoding="utf-8") as f:
88
+ f.write(json.dumps(record, sort_keys=True) + "\n")
89
+ return sequence_id
90
+
91
+ def _next_sequence_id(self) -> int:
92
+ if not self.ledger_path.exists():
93
+ return 1
94
+ last = 0
95
+ with self.ledger_path.open("r", encoding="utf-8") as f:
96
+ for line in f:
97
+ line = line.strip()
98
+ if not line:
99
+ continue
100
+ try:
101
+ rec = json.loads(line)
102
+ last = max(last, rec.get("sequence_id", 0))
103
+ except json.JSONDecodeError:
104
+ continue
105
+ return last + 1
106
+
107
+ def read_all(self) -> List[dict]:
108
+ """Read all records in deterministic order (sequence_id, timestamp)."""
109
+ if not self.ledger_path.exists():
110
+ return []
111
+ records = []
112
+ with self.ledger_path.open("r", encoding="utf-8") as f:
113
+ for line in f:
114
+ line = line.strip()
115
+ if not line:
116
+ continue
117
+ try:
118
+ records.append(json.loads(line))
119
+ except json.JSONDecodeError:
120
+ continue
121
+ records.sort(key=lambda r: (r.get("sequence_id", 0), r.get("timestamp", "")))
122
+ return records
123
+
124
+ def replay_checksum(self) -> str:
125
+ """Deterministic checksum of ledger for replay verification."""
126
+ records = self.read_all()
127
+ content = json.dumps(records, sort_keys=True)
128
+ return hashlib.sha256(content.encode("utf-8")).hexdigest()
129
+
130
+
131
+ def create_envelope(agent_id: str, round_id: int, payload: Any, trust: float = 0.5) -> REPEnvelope:
132
+ """Build REP envelope from payload (hash stored; payload not stored in envelope)."""
133
+ payload_hash = _hash_payload(payload)
134
+ return REPEnvelope(agent_id=agent_id, round_id=round_id, trust=trust, payload_hash=payload_hash)
@@ -0,0 +1,25 @@
1
+ """
2
+ devtorch_core.rep_network — S15 multi-node REP ledger synchronization.
3
+ """
4
+
5
+ from devtorch_core.rep_network.node import NodeIdentity, NodeRegistry, PeerEntry
6
+ from devtorch_core.rep_network.sync import (
7
+ SyncResult,
8
+ pull_from_peer,
9
+ push_to_peer,
10
+ sync_all_peers,
11
+ sync_with_peer,
12
+ )
13
+ from devtorch_core.rep_network.merge import merge_theta
14
+
15
+ __all__ = [
16
+ "NodeIdentity",
17
+ "PeerEntry",
18
+ "NodeRegistry",
19
+ "SyncResult",
20
+ "push_to_peer",
21
+ "pull_from_peer",
22
+ "sync_with_peer",
23
+ "sync_all_peers",
24
+ "merge_theta",
25
+ ]
@@ -0,0 +1,70 @@
1
+ """
2
+ theta.json merge function for REP network synchronisation.
3
+ """
4
+ from __future__ import annotations
5
+ from typing import Dict
6
+
7
+ _LEVEL_RANK = {"PUBLIC": 0, "PROTECTED": 1, "PRIVATE": 2}
8
+ _RANK_LEVEL = {0: "PUBLIC", 1: "PROTECTED", 2: "PRIVATE"}
9
+
10
+
11
+ def merge_theta(local: dict, remote: dict) -> dict:
12
+ """
13
+ Merge two theta.json dicts (remote into local) and return the merged result.
14
+
15
+ Merge rules per concept:
16
+ - event_count: sum of both
17
+ - mean_confidence: weighted mean (weighted by event_count from each side)
18
+ - disclosure_level: max (most restrictive) of both sides
19
+ - last_updated: take the more recent of both (ISO string comparison)
20
+
21
+ The returned dict has the same shape as ThetaStore.load() output:
22
+ {
23
+ "version": 1,
24
+ "updated_at": <now ISO UTC>,
25
+ "coordination_vector": { concept: { ... } }
26
+ }
27
+ """
28
+ import datetime as _dt
29
+
30
+ local_cv = local.get("coordination_vector", {})
31
+ remote_cv = remote.get("coordination_vector", {})
32
+ all_concepts = set(local_cv) | set(remote_cv)
33
+
34
+ merged_cv: Dict[str, dict] = {}
35
+ for concept in all_concepts:
36
+ l = local_cv.get(concept, {})
37
+ r = remote_cv.get(concept, {})
38
+
39
+ l_count = int(l.get("event_count", 0))
40
+ r_count = int(r.get("event_count", 0))
41
+ total = l_count + r_count
42
+
43
+ if total == 0:
44
+ merged_conf = 0.0
45
+ else:
46
+ l_conf = float(l.get("mean_confidence", 0.0))
47
+ r_conf = float(r.get("mean_confidence", 0.0))
48
+ merged_conf = round((l_conf * l_count + r_conf * r_count) / total, 6)
49
+
50
+ l_rank = _LEVEL_RANK.get(l.get("disclosure_level", "PUBLIC"), 0)
51
+ r_rank = _LEVEL_RANK.get(r.get("disclosure_level", "PUBLIC"), 0)
52
+ merged_level = _RANK_LEVEL[max(l_rank, r_rank)]
53
+
54
+ l_ts = l.get("last_updated", "")
55
+ r_ts = r.get("last_updated", "")
56
+ merged_ts = l_ts if l_ts >= r_ts else r_ts
57
+
58
+ merged_cv[concept] = {
59
+ "event_count": total,
60
+ "mean_confidence": merged_conf,
61
+ "disclosure_level": merged_level,
62
+ "last_updated": merged_ts,
63
+ }
64
+
65
+ now = _dt.datetime.now(tz=_dt.timezone.utc).isoformat()
66
+ return {
67
+ "version": local.get("version", 1),
68
+ "updated_at": now,
69
+ "coordination_vector": merged_cv,
70
+ }
@@ -0,0 +1,137 @@
1
+ """
2
+ REP network node identity and peer discovery.
3
+
4
+ Nodes are identified by a UUID generated once and stored in .GCC/rep/node_id.
5
+ Peer discovery is file-based (no mDNS/DNS-SD in S15): peers are listed in
6
+ .GCC/rep/peers.json as [{id: str, url: str, name: str}].
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import socket
13
+ import uuid
14
+ from dataclasses import dataclass, field
15
+ from datetime import datetime, timezone
16
+ from pathlib import Path
17
+ from typing import List
18
+
19
+
20
+ @dataclass
21
+ class NodeIdentity:
22
+ """Identity of this node."""
23
+ node_id: str
24
+ name: str
25
+ url: str
26
+
27
+
28
+ @dataclass
29
+ class PeerEntry:
30
+ """A known remote peer."""
31
+ node_id: str
32
+ name: str
33
+ url: str
34
+ last_seen: str = ""
35
+
36
+
37
+ class NodeRegistry:
38
+ """Manages node identity and peer list backed by .GCC/rep/ files."""
39
+
40
+ def __init__(self, gcc_root: str) -> None:
41
+ self._rep_dir = Path(gcc_root) / "rep"
42
+
43
+ # ------------------------------------------------------------------
44
+ # Identity
45
+ # ------------------------------------------------------------------
46
+
47
+ def get_or_create_identity(self, url: str = "") -> NodeIdentity:
48
+ """Read .GCC/rep/node_id; create and persist one if missing."""
49
+ self._rep_dir.mkdir(parents=True, exist_ok=True)
50
+ node_id_path = self._rep_dir / "node_id"
51
+ if node_id_path.exists():
52
+ try:
53
+ node_id = node_id_path.read_text(encoding="utf-8").strip()
54
+ except OSError:
55
+ node_id = str(uuid.uuid4())
56
+ node_id_path.write_text(node_id, encoding="utf-8")
57
+ else:
58
+ node_id = str(uuid.uuid4())
59
+ node_id_path.write_text(node_id, encoding="utf-8")
60
+
61
+ name = socket.gethostname()
62
+ return NodeIdentity(node_id=node_id, name=name, url=url)
63
+
64
+ # ------------------------------------------------------------------
65
+ # Peers
66
+ # ------------------------------------------------------------------
67
+
68
+ def _peers_path(self) -> Path:
69
+ return self._rep_dir / "peers.json"
70
+
71
+ def list_peers(self) -> List[PeerEntry]:
72
+ """Read .GCC/rep/peers.json; return empty list if missing or corrupt."""
73
+ path = self._peers_path()
74
+ if not path.exists():
75
+ return []
76
+ try:
77
+ raw = json.loads(path.read_text(encoding="utf-8"))
78
+ if not isinstance(raw, list):
79
+ return []
80
+ peers = []
81
+ for item in raw:
82
+ if not isinstance(item, dict):
83
+ continue
84
+ try:
85
+ peers.append(PeerEntry(
86
+ node_id=str(item["node_id"]),
87
+ name=str(item.get("name", "")),
88
+ url=str(item.get("url", "")),
89
+ last_seen=str(item.get("last_seen", "")),
90
+ ))
91
+ except (KeyError, TypeError):
92
+ continue
93
+ return peers
94
+ except (json.JSONDecodeError, OSError):
95
+ return []
96
+
97
+ def _save_peers(self, peers: List[PeerEntry]) -> None:
98
+ self._rep_dir.mkdir(parents=True, exist_ok=True)
99
+ data = [
100
+ {
101
+ "node_id": p.node_id,
102
+ "name": p.name,
103
+ "url": p.url,
104
+ "last_seen": p.last_seen,
105
+ }
106
+ for p in peers
107
+ ]
108
+ self._peers_path().write_text(json.dumps(data, indent=2), encoding="utf-8")
109
+
110
+ def add_peer(self, peer: PeerEntry) -> None:
111
+ """Append peer; replace existing entry with the same node_id."""
112
+ peers = self.list_peers()
113
+ peers = [p for p in peers if p.node_id != peer.node_id]
114
+ peers.append(peer)
115
+ self._save_peers(peers)
116
+
117
+ def remove_peer(self, node_id: str) -> None:
118
+ """Remove peer by node_id; no-op if not found."""
119
+ peers = [p for p in self.list_peers() if p.node_id != node_id]
120
+ self._save_peers(peers)
121
+
122
+ def update_peer_last_seen(self, node_id: str) -> None:
123
+ """Set last_seen to current UTC ISO timestamp for the given peer."""
124
+ peers = self.list_peers()
125
+ ts = datetime.now(timezone.utc).isoformat()
126
+ updated = []
127
+ for p in peers:
128
+ if p.node_id == node_id:
129
+ updated.append(PeerEntry(
130
+ node_id=p.node_id,
131
+ name=p.name,
132
+ url=p.url,
133
+ last_seen=ts,
134
+ ))
135
+ else:
136
+ updated.append(p)
137
+ self._save_peers(updated)
@@ -0,0 +1,140 @@
1
+ """
2
+ FastAPI server exposing REP ledger to peers.
3
+ Runs on a different port from the proxy (default 8767).
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any, Dict, List
9
+
10
+ from devtorch_core.rep import REPEnvelope, REPLedger
11
+ from devtorch_core.rep_network.node import NodeRegistry, PeerEntry
12
+
13
+ try:
14
+ from fastapi import Body, FastAPI
15
+ from pydantic import BaseModel
16
+
17
+ HAS_FASTAPI = True
18
+
19
+ class PushEntriesRequest(BaseModel):
20
+ entries: List[Dict[str, Any]]
21
+ sender_id: str = ""
22
+
23
+ class PeerEntryModel(BaseModel):
24
+ node_id: str
25
+ name: str = ""
26
+ url: str = ""
27
+ last_seen: str = ""
28
+
29
+ except ImportError:
30
+ HAS_FASTAPI = False
31
+
32
+
33
+ def create_rep_server_app(ledger: REPLedger, registry: NodeRegistry) -> "FastAPI":
34
+ """Build and return a FastAPI application exposing the REP ledger.
35
+
36
+ Raises RuntimeError if FastAPI is not installed.
37
+ """
38
+ if not HAS_FASTAPI:
39
+ raise RuntimeError("FastAPI is not installed; cannot create REP server app.")
40
+
41
+ app = FastAPI(title="DevTorch REP Network Node", version="0.1.0")
42
+
43
+ # ------------------------------------------------------------------
44
+ # Helpers
45
+ # ------------------------------------------------------------------
46
+
47
+ from devtorch_core.rep_network.sync import _envelope_ids_in_ledger, _persist_pulled_id
48
+
49
+ def _existing_envelope_ids() -> set:
50
+ return _envelope_ids_in_ledger(ledger)
51
+
52
+ def _get_node_id() -> str:
53
+ try:
54
+ identity = registry.get_or_create_identity()
55
+ return identity.node_id
56
+ except Exception: # noqa: BLE001
57
+ return ""
58
+
59
+ # ------------------------------------------------------------------
60
+ # Routes
61
+ # ------------------------------------------------------------------
62
+
63
+ @app.get("/rep/entries")
64
+ def get_entries(since: int = 0) -> Dict[str, Any]:
65
+ """Return ledger entries with sequence_id > since."""
66
+ all_records = ledger.read_all()
67
+ filtered = [r for r in all_records if r.get("sequence_id", 0) > since]
68
+ entries = [r.get("envelope", {}) for r in filtered]
69
+ return {
70
+ "node_id": _get_node_id(),
71
+ "entries": entries,
72
+ "count": len(entries),
73
+ }
74
+
75
+ @app.post("/rep/entries")
76
+ def post_entries(request: PushEntriesRequest = Body(...)) -> Dict[str, Any]:
77
+ """Accept pushed entries; skip duplicates by envelope_id."""
78
+ existing_ids = _existing_envelope_ids()
79
+ accepted = 0
80
+ duplicate = 0
81
+ import datetime as _dt
82
+
83
+ for entry_dict in request.entries:
84
+ eid = entry_dict.get("envelope_id")
85
+ if eid and eid in existing_ids:
86
+ duplicate += 1
87
+ continue
88
+ try:
89
+ envelope = REPEnvelope.from_dict(entry_dict)
90
+ except Exception: # noqa: BLE001
91
+ continue
92
+ ts = entry_dict.get("timestamp", _dt.datetime.now(_dt.timezone.utc).isoformat())
93
+ ledger.append(envelope, timestamp=ts)
94
+ if eid:
95
+ existing_ids.add(eid)
96
+ _persist_pulled_id(ledger, eid)
97
+ accepted += 1
98
+
99
+ # Update last_seen for sender
100
+ if request.sender_id:
101
+ try:
102
+ registry.update_peer_last_seen(request.sender_id)
103
+ except Exception: # noqa: BLE001
104
+ pass
105
+
106
+ return {"accepted": accepted, "duplicate": duplicate}
107
+
108
+ @app.get("/rep/peers")
109
+ def get_peers() -> List[Dict[str, Any]]:
110
+ peers = registry.list_peers()
111
+ return [
112
+ {
113
+ "node_id": p.node_id,
114
+ "name": p.name,
115
+ "url": p.url,
116
+ "last_seen": p.last_seen,
117
+ }
118
+ for p in peers
119
+ ]
120
+
121
+ @app.post("/rep/peers")
122
+ def post_peer(body: PeerEntryModel = Body(...)) -> Dict[str, str]:
123
+ peer = PeerEntry(
124
+ node_id=body.node_id,
125
+ name=body.name,
126
+ url=body.url,
127
+ last_seen=body.last_seen,
128
+ )
129
+ registry.add_peer(peer)
130
+ return {"status": "ok"}
131
+
132
+ @app.get("/health")
133
+ def health() -> Dict[str, Any]:
134
+ return {
135
+ "status": "ok",
136
+ "node_id": _get_node_id(),
137
+ "entry_count": len(ledger.read_all()),
138
+ }
139
+
140
+ return app