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,555 @@
1
+ """
2
+ Sprint 29 — GitHub PR reasoning-delta comment and merge gate.
3
+
4
+ Builds a markdown comment that surfaces Θ changes, sensitivity events, new
5
+ concepts, MCS score/trend, and invariant status for a pull request. Also
6
+ provides a merge gate that blocks merge when I3 mismatches on critical
7
+ concepts, MCS is too low, or a consensus lock is active.
8
+
9
+ No external HTTP dependencies: `post_pr_comment` uses only the standard
10
+ library `urllib`.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import urllib.request
17
+ from dataclasses import dataclass, field
18
+ from datetime import datetime, timezone
19
+ from pathlib import Path
20
+ from typing import Any, Dict, List, Optional, Tuple
21
+
22
+
23
+
24
+
25
+ try:
26
+ from devtorch_core import GCCRepository
27
+ except ImportError: # pragma: no cover
28
+ GCCRepository = None # type: ignore[misc,assignment]
29
+
30
+
31
+ @dataclass
32
+ class MergeGateResult:
33
+ """Result of a merge-gate check."""
34
+
35
+ allowed: bool
36
+ reasons: List[str] = field(default_factory=list)
37
+ summary: str = ""
38
+
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # Internal helpers
42
+ # ---------------------------------------------------------------------------
43
+
44
+
45
+ def _load_commit(repo: Any, commit_id: Optional[str]) -> Optional[dict]:
46
+ """Load a commit object from .GCC/commits/<id>.json if it exists."""
47
+ if not commit_id or not hasattr(repo, "gcc_dir"):
48
+ return None
49
+ commit_path = repo.gcc_dir / "commits" / f"{commit_id}.json"
50
+ if not commit_path.exists():
51
+ return None
52
+ try:
53
+ return json.loads(commit_path.read_text(encoding="utf-8"))
54
+ except (json.JSONDecodeError, OSError):
55
+ return None
56
+
57
+
58
+ def _branch_tip(repo: Any, branch: str) -> Optional[str]:
59
+ """Return the commit id at the tip of *branch*, or None."""
60
+ if not hasattr(repo, "_read_branch_tip"):
61
+ return None
62
+ return repo._read_branch_tip(branch)
63
+
64
+
65
+ def _now_utc() -> str:
66
+ return datetime.now(timezone.utc).isoformat()
67
+
68
+
69
+ def _reconstruct_theta(events: List[dict]) -> Dict[str, dict]:
70
+ """
71
+ Reconstruct a Θ coordination vector from a list of sensitivity event dicts.
72
+ Uses the same rules as RulesBasedAggPhi + ThetaStore.ripple.
73
+ """
74
+ from devtorch_core.theta import RulesBasedAggPhi
75
+
76
+ aggregator = RulesBasedAggPhi()
77
+ delta = aggregator.aggregate(events)
78
+ theta: Dict[str, dict] = {}
79
+ for concept, entry in delta.items():
80
+ theta[concept] = dict(entry)
81
+ theta[concept]["last_updated"] = entry.get("last_updated") or _now_utc()
82
+ return theta
83
+
84
+
85
+ def _compare_theta(
86
+ base: Dict[str, dict],
87
+ head: Dict[str, dict],
88
+ ) -> Tuple[List[str], List[str], List[Tuple[str, dict, dict]]]:
89
+ """
90
+ Compare two Θ coordination vectors.
91
+
92
+ Returns:
93
+ (added_concepts, removed_concepts, modified_concepts)
94
+ where modified_concepts is a list of (concept, base_entry, head_entry).
95
+ """
96
+ base_keys = set(base.keys())
97
+ head_keys = set(head.keys())
98
+ added = sorted(head_keys - base_keys)
99
+ removed = sorted(base_keys - head_keys)
100
+ modified: List[Tuple[str, dict, dict]] = []
101
+ for concept in sorted(base_keys & head_keys):
102
+ b = base[concept]
103
+ h = head[concept]
104
+ if any(
105
+ b.get(k) != h.get(k)
106
+ for k in ("mean_confidence", "disclosure_level", "event_count")
107
+ ):
108
+ modified.append((concept, b, h))
109
+ return added, removed, modified
110
+
111
+
112
+ def _split_events_by_cutoff(
113
+ events: List[dict],
114
+ cutoff: Optional[str],
115
+ ) -> Tuple[List[dict], List[dict]]:
116
+ """Split events into (before_or_at_cutoff, after_cutoff)."""
117
+ if not cutoff:
118
+ return [], events
119
+ before: List[dict] = []
120
+ after: List[dict] = []
121
+ for ev in events:
122
+ ts = ev.get("created_at", "") or ""
123
+ if ts and ts < cutoff:
124
+ before.append(ev)
125
+ else:
126
+ after.append(ev)
127
+ return before, after
128
+
129
+
130
+ def _compute_mcs_proxy(events: List[dict]) -> float:
131
+ """
132
+ Compute a proxy MCS score from sensitivity events.
133
+
134
+ Since the repo does not store explicit CollisionEvents, we derive a proxy:
135
+ severity = 1.0 - confidence, resolution_minutes = 0. The returned value is
136
+ intentionally simple; it is a design assumption documented in the comment.
137
+ """
138
+ from devtorch_core.metrics import CollisionEvent, compute_mcs
139
+
140
+ if not events:
141
+ return 0.0
142
+ collisions = [
143
+ CollisionEvent(
144
+ timestamp=ev.get("created_at", _now_utc()),
145
+ branch_a="head",
146
+ branch_b="base",
147
+ concept=ev.get("target_concept", "unknown"),
148
+ severity=max(0.0, min(1.0, 1.0 - float(ev.get("confidence", 0.5)))),
149
+ resolution_minutes=0.0,
150
+ )
151
+ for ev in events
152
+ ]
153
+ return float(compute_mcs(collisions))
154
+
155
+
156
+ def _load_sensitivity_events(repo: Any) -> List[dict]:
157
+ """Return all sensitivity events from the repo as dicts."""
158
+ if not hasattr(repo, "list_sensitivities"):
159
+ return []
160
+ return repo.list_sensitivities()
161
+
162
+
163
+ def _load_concepts(repo: Any) -> List[str]:
164
+ """Return all defined concept names from the repo."""
165
+ if not hasattr(repo, "concept_list"):
166
+ return []
167
+ return repo.concept_list()
168
+
169
+
170
+ def _run_invariants(
171
+ repo: Any,
172
+ base_branch: str,
173
+ head_branch: str,
174
+ critical_concepts: List[str],
175
+ ) -> List[Any]:
176
+ """Run I1/I3 invariants for the merge of base into head."""
177
+ if not hasattr(repo, "invariant_check"):
178
+ return []
179
+ base_tip = _branch_tip(repo, base_branch)
180
+ head_tip = _branch_tip(repo, head_branch)
181
+ branch_tips = {base_branch: base_tip or "", head_branch: head_tip or ""}
182
+ return repo.invariant_check(
183
+ operation="merge",
184
+ branch_tips=branch_tips,
185
+ concepts_used=critical_concepts,
186
+ )
187
+
188
+
189
+ # ---------------------------------------------------------------------------
190
+ # Public API
191
+ # ---------------------------------------------------------------------------
192
+
193
+
194
+ def build_reasoning_delta_comment(
195
+ repo: Any,
196
+ base_branch: str,
197
+ head_branch: str,
198
+ ) -> str:
199
+ """
200
+ Generate a markdown PR comment describing the reasoning delta between
201
+ *base_branch* and *head_branch*.
202
+
203
+ Assumptions
204
+ -----------
205
+ - The repo stores a single current Θ vector; the "base" Θ is reconstructed
206
+ by replaying sensitivity events whose timestamp is older than the head
207
+ branch tip commit timestamp.
208
+ - MCS is a proxy derived from sensitivity-event confidence (severity = 1 -
209
+ confidence). True collision events are not persisted yet.
210
+ - New concepts are those defined in the concept store and referenced by
211
+ sensitivity events introduced on the head branch.
212
+ """
213
+ lines: List[str] = []
214
+
215
+ # ── Header ──────────────────────────────────────────────────────────────
216
+ lines.append("## 🛡️ DevTorch Reasoning Delta")
217
+ lines.append("")
218
+ lines.append(f"Comparing `{base_branch}` (base) → `{head_branch}` (head).")
219
+ lines.append("")
220
+
221
+ # ── Branch tips ─────────────────────────────────────────────────────────
222
+ base_tip = _branch_tip(repo, base_branch)
223
+ head_tip = _branch_tip(repo, head_branch)
224
+ head_commit = _load_commit(repo, head_tip)
225
+ head_ts = head_commit.get("timestamp") if head_commit else None
226
+
227
+ lines.append("| Branch | Tip |")
228
+ lines.append("|---|---|")
229
+ lines.append(f"| `{base_branch}` | `{base_tip or 'none'}` |")
230
+ lines.append(f"| `{head_branch}` | `{head_tip or 'none'}` |")
231
+ lines.append("")
232
+
233
+ # ── Θ changes ─────────────────────────────────────────────────────────────
234
+ theta = repo.get_theta() if hasattr(repo, "get_theta") else {"coordination_vector": {}}
235
+ head_theta = theta.get("coordination_vector", {})
236
+
237
+ all_events = _load_sensitivity_events(repo)
238
+ base_events, head_events = _split_events_by_cutoff(all_events, head_ts)
239
+ base_theta = _reconstruct_theta(base_events)
240
+
241
+ added, removed, modified = _compare_theta(base_theta, head_theta)
242
+
243
+ lines.append("### Θ Changes")
244
+ lines.append("")
245
+ if added:
246
+ lines.append("**Added concepts:**")
247
+ for c in added:
248
+ entry = head_theta.get(c, {})
249
+ conf = entry.get("mean_confidence", 0.0)
250
+ dl = entry.get("disclosure_level", "PUBLIC")
251
+ lines.append(f"- `{c}` — confidence {conf:.2f}, disclosure `{dl}`")
252
+ lines.append("")
253
+ if removed:
254
+ lines.append("**Removed concepts:**")
255
+ for c in removed:
256
+ lines.append(f"- `{c}`")
257
+ lines.append("")
258
+ if modified:
259
+ lines.append("**Modified concepts:**")
260
+ for c, b, h in modified:
261
+ bc = b.get("mean_confidence", 0.0)
262
+ hc = h.get("mean_confidence", 0.0)
263
+ bd = b.get("disclosure_level", "PUBLIC")
264
+ hd = h.get("disclosure_level", "PUBLIC")
265
+ be = b.get("event_count", 0)
266
+ he = h.get("event_count", 0)
267
+ lines.append(
268
+ f"- `{c}` — confidence {bc:.2f} → {hc:.2f}, "
269
+ f"disclosure `{bd}` → `{hd}`, events {be} → {he}"
270
+ )
271
+ lines.append("")
272
+ if not (added or removed or modified):
273
+ lines.append("_No Θ changes detected between base and head._")
274
+ lines.append("")
275
+
276
+ # ── Sensitivity events added in this PR ─────────────────────────────────
277
+ lines.append("### Sensitivity Events Added in This PR")
278
+ lines.append("")
279
+ if head_events:
280
+ lines.append(f"**{len(head_events)}** new sensitivity event(s).")
281
+ lines.append("")
282
+ for ev in head_events:
283
+ concept = ev.get("target_concept", "unknown")
284
+ conf = ev.get("confidence", 0.0)
285
+ dl = ev.get("disclosure_level", "PUBLIC")
286
+ src = ev.get("source_node", "unknown")
287
+ lines.append(
288
+ f"- `{concept}` — source `{src}`, confidence `{conf:.2f}`, disclosure `{dl}`"
289
+ )
290
+ lines.append("")
291
+ else:
292
+ lines.append("_No new sensitivity events detected._")
293
+ lines.append("")
294
+
295
+ # ── New concepts defined ──────────────────────────────────────────────────
296
+ defined_concepts = set(_load_concepts(repo))
297
+ new_concepts = sorted(
298
+ (set(added) & defined_concepts)
299
+ | {
300
+ ev.get("target_concept", "")
301
+ for ev in head_events
302
+ if ev.get("target_concept", "") in defined_concepts
303
+ }
304
+ )
305
+ lines.append("### New Concepts Defined")
306
+ lines.append("")
307
+ if new_concepts:
308
+ for c in new_concepts:
309
+ lines.append(f"- `{c}`")
310
+ lines.append("")
311
+ else:
312
+ lines.append("_No new concept definitions detected._")
313
+ lines.append("")
314
+
315
+ # ── MCS score and trend ─────────────────────────────────────────────────
316
+ head_mcs = _compute_mcs_proxy(head_events)
317
+ base_mcs = _compute_mcs_proxy(base_events)
318
+ if head_mcs > base_mcs:
319
+ trend = "↗️ rising"
320
+ elif head_mcs < base_mcs:
321
+ trend = "↘️ falling"
322
+ else:
323
+ trend = "➡️ stable"
324
+
325
+ lines.append("### MCS Score & Trend")
326
+ lines.append("")
327
+ lines.append(f"- **Head MCS:** `{head_mcs:.2f}`")
328
+ lines.append(f"- **Base MCS:** `{base_mcs:.2f}`")
329
+ lines.append(f"- **Trend:** {trend}")
330
+ lines.append("")
331
+
332
+ # ── Invariant status (I1/I3) at head ────────────────────────────────────
333
+ failures = _run_invariants(repo, base_branch, head_branch, [])
334
+ i1_failures = [f for f in failures if getattr(f, "invariant_id", "") == "I1"]
335
+ i3_failures = [f for f in failures if getattr(f, "invariant_id", "") == "I3"]
336
+
337
+ lines.append("### Invariant Status at Head")
338
+ lines.append("")
339
+ i1_status = "✅ OK" if not i1_failures else "❌ FAILED"
340
+ i3_status = "✅ OK" if not i3_failures else "❌ FAILED"
341
+ lines.append(f"- **I1 (Commit-backed):** {i1_status} ({len(i1_failures)} failure(s))")
342
+ lines.append(f"- **I3 (Semantic grounding):** {i3_status} ({len(i3_failures)} failure(s))")
343
+ lines.append("")
344
+ if failures:
345
+ lines.append("**Failures:**")
346
+ for f in failures:
347
+ inv_id = getattr(f, "invariant_id", "?")
348
+ msg = getattr(f, "message", str(f))
349
+ fix = getattr(f, "actionable_fix", "")
350
+ lines.append(f"- `{inv_id}`: {msg}")
351
+ if fix:
352
+ lines.append(f" - Fix: {fix}")
353
+ lines.append("")
354
+
355
+ # ── Assumptions ───────────────────────────────────────────────────────────
356
+ lines.append("### Assumptions")
357
+ lines.append("")
358
+ lines.append(
359
+ "1. Base Θ is reconstructed by replaying sensitivity events whose "
360
+ "`created_at` is older than the head branch tip commit timestamp."
361
+ )
362
+ lines.append(
363
+ "2. MCS is a proxy derived from sensitivity-event confidence "
364
+ "(`severity = 1 - confidence`). True collision events are not persisted."
365
+ )
366
+ lines.append(
367
+ "3. New concepts are concept-store definitions referenced by head-branch "
368
+ "sensitivity events or newly added Θ concepts."
369
+ )
370
+ lines.append(
371
+ "4. The merge gate below uses the same invariant context as a "
372
+ "fast-forward merge of base into head."
373
+ )
374
+ lines.append("")
375
+
376
+ # ── Footer ───────────────────────────────────────────────────────────────
377
+ lines.append(
378
+ "---\n_Generated by DevTorch | "
379
+ "[docs](https://github.com/flotorch-ai/devtorch)_"
380
+ )
381
+
382
+ return "\n".join(lines)
383
+
384
+
385
+ def check_merge_gate(
386
+ repo: Any,
387
+ critical_concepts: List[str],
388
+ min_mcs: float,
389
+ ) -> MergeGateResult:
390
+ """
391
+ Check whether a PR can merge.
392
+
393
+ Gate conditions:
394
+ - No unresolved I3 mismatches on *critical_concepts*.
395
+ - MCS score >= *min_mcs* (when min_mcs > 0).
396
+ - No unresolved consensus lock.
397
+ """
398
+ reasons: List[str] = []
399
+ current_branch = (
400
+ repo._current_branch()
401
+ if hasattr(repo, "_current_branch")
402
+ else "main"
403
+ )
404
+
405
+ # I3 on critical concepts
406
+ failures = _run_invariants(
407
+ repo,
408
+ base_branch=current_branch,
409
+ head_branch=current_branch,
410
+ critical_concepts=critical_concepts,
411
+ )
412
+ i3_failures = [f for f in failures if getattr(f, "invariant_id", "") == "I3"]
413
+ if i3_failures:
414
+ concepts_missing = [f"`{getattr(f, 'message', str(f))}`" for f in i3_failures]
415
+ reasons.append(
416
+ "I3 semantic-grounding failures on critical concepts: "
417
+ + ", ".join(concepts_missing)
418
+ )
419
+
420
+ # MCS threshold
421
+ if min_mcs > 0:
422
+ all_events = _load_sensitivity_events(repo)
423
+ current_mcs = _compute_mcs_proxy(all_events)
424
+ if current_mcs < min_mcs:
425
+ reasons.append(
426
+ f"MCS score {current_mcs:.2f} is below required minimum {min_mcs:.2f}"
427
+ )
428
+
429
+ # Consensus lock
430
+ if hasattr(repo, "is_locked") and repo.is_locked():
431
+ reasons.append("Consensus lock is active; unlock before merging")
432
+
433
+ allowed = not reasons
434
+ if allowed:
435
+ summary = "Merge gate passed: no I3 mismatches, MCS OK, and no consensus lock."
436
+ else:
437
+ summary = "Merge gate blocked: " + "; ".join(reasons)
438
+
439
+ return MergeGateResult(allowed=allowed, reasons=reasons, summary=summary)
440
+
441
+
442
+ def post_pr_comment(
443
+ repo_owner: str,
444
+ repo_name: str,
445
+ pr_number: int,
446
+ token: str,
447
+ body: str,
448
+ ) -> bool:
449
+ """
450
+ Post *body* as a comment on a GitHub PR using only the standard library.
451
+
452
+ Returns True on success (HTTP 2xx), False otherwise.
453
+ """
454
+ url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/issues/{pr_number}/comments"
455
+ payload = json.dumps({"body": body}).encode("utf-8")
456
+ req = urllib.request.Request(
457
+ url,
458
+ data=payload,
459
+ method="POST",
460
+ headers={
461
+ "Authorization": f"Bearer {token}",
462
+ "Accept": "application/vnd.github+json",
463
+ "User-Agent": "DevTorch-PR-Reporter",
464
+ "Content-Type": "application/json",
465
+ },
466
+ )
467
+ try:
468
+ with urllib.request.urlopen(req, timeout=15) as resp:
469
+ return 200 <= resp.status < 300
470
+ except Exception:
471
+ return False
472
+
473
+
474
+ def run_pr_reporter(
475
+ gcc_repo_path: str,
476
+ base_branch: str,
477
+ head_branch: str,
478
+ repo_owner: str,
479
+ repo_name: str,
480
+ pr_number: int,
481
+ token: str,
482
+ critical_concepts: List[str],
483
+ min_mcs: float,
484
+ ) -> MergeGateResult:
485
+ """
486
+ Orchestrate building the reasoning-delta comment and merge gate, post the
487
+ comment, and return the gate result.
488
+ """
489
+ if GCCRepository is None:
490
+ raise RuntimeError("GCCRepository is not available")
491
+
492
+ repo = GCCRepository.at(Path(gcc_repo_path))
493
+ if not repo.is_initialized():
494
+ raise RuntimeError(f"GCC repository at {gcc_repo_path} is not initialized")
495
+
496
+ comment = build_reasoning_delta_comment(repo, base_branch, head_branch)
497
+ post_pr_comment(repo_owner, repo_name, pr_number, token, comment)
498
+
499
+ return check_merge_gate(repo, critical_concepts, min_mcs)
500
+
501
+
502
+ def main() -> None:
503
+ """
504
+ Lightweight CLI helper for manual invocation.
505
+
506
+ Usage example:
507
+ python -m devtorch_core.github.pr_reporter \
508
+ /path/to/repo base head owner name 42 token "auth,schema" 0.5
509
+ """
510
+ import sys
511
+
512
+ args = sys.argv[1:]
513
+ if len(args) != 9:
514
+ print(
515
+ "Usage: python -m devtorch_core.github.pr_reporter "
516
+ "<gcc_repo_path> <base_branch> <head_branch> "
517
+ "<repo_owner> <repo_name> <pr_number> <token> "
518
+ "<critical_concepts_csv> <min_mcs>",
519
+ file=sys.stderr,
520
+ )
521
+ sys.exit(1)
522
+
523
+ (
524
+ gcc_repo_path,
525
+ base_branch,
526
+ head_branch,
527
+ repo_owner,
528
+ repo_name,
529
+ pr_number_str,
530
+ token,
531
+ critical_concepts_csv,
532
+ min_mcs_str,
533
+ ) = args
534
+
535
+ pr_number = int(pr_number_str)
536
+ critical_concepts = [c.strip() for c in critical_concepts_csv.split(",") if c.strip()]
537
+ min_mcs = float(min_mcs_str)
538
+
539
+ result = run_pr_reporter(
540
+ gcc_repo_path=gcc_repo_path,
541
+ base_branch=base_branch,
542
+ head_branch=head_branch,
543
+ repo_owner=repo_owner,
544
+ repo_name=repo_name,
545
+ pr_number=pr_number,
546
+ token=token,
547
+ critical_concepts=critical_concepts,
548
+ min_mcs=min_mcs,
549
+ )
550
+ print(result.summary)
551
+ sys.exit(0 if result.allowed else 1)
552
+
553
+
554
+ if __name__ == "__main__":
555
+ main()