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,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()
|