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,35 @@
1
+ """
2
+ devtorch_core.github — GitHub App + CI/CD integration (Sprint 12).
3
+
4
+ Public API:
5
+ parse_pr_body — parse a PR body string into PRMetadata
6
+ PRMetadata — dataclass holding extracted governance metadata
7
+ build_pr_comment — render a GovernanceSummary as a GitHub PR comment
8
+ GovernanceSummary — dataclass holding governance state for comment rendering
9
+ """
10
+
11
+ from .pr_parser import PRMetadata, parse_pr_body
12
+ from .comment_builder import GovernanceSummary, build_pr_comment
13
+ from .pat import GitHubPATStore
14
+ from .app import GitHubAppHandler
15
+ from .pr_reporter import (
16
+ MergeGateResult,
17
+ build_reasoning_delta_comment,
18
+ check_merge_gate,
19
+ post_pr_comment,
20
+ run_pr_reporter,
21
+ )
22
+
23
+ __all__ = [
24
+ "parse_pr_body",
25
+ "PRMetadata",
26
+ "build_pr_comment",
27
+ "GovernanceSummary",
28
+ "GitHubPATStore",
29
+ "GitHubAppHandler",
30
+ "MergeGateResult",
31
+ "build_reasoning_delta_comment",
32
+ "check_merge_gate",
33
+ "post_pr_comment",
34
+ "run_pr_reporter",
35
+ ]
@@ -0,0 +1,240 @@
1
+ """
2
+ GitHub App webhook receiver for DevTorch.
3
+
4
+ Receives GitHub webhook events, validates HMAC signatures, and extracts
5
+ DevTorch governance metadata from PR bodies.
6
+
7
+ FastAPI is optional — the module degrades gracefully when it is not installed.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ import hmac
14
+ import json
15
+ import logging
16
+ import os
17
+ import time
18
+ import urllib.request
19
+ from typing import Any, Dict, Optional
20
+
21
+ from .pr_parser import parse_pr_body
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # ── Optional FastAPI import ───────────────────────────────────────────────────
26
+ try:
27
+ from fastapi import FastAPI, Request, Response
28
+
29
+ HAS_FASTAPI = True
30
+ except ImportError: # pragma: no cover
31
+ HAS_FASTAPI = False
32
+
33
+ _WEBHOOK_SECRET_ENV = "DEVTORCH_GITHUB_WEBHOOK_SECRET"
34
+
35
+
36
+ def _verify_signature(payload: bytes, signature_header: str, secret: str) -> bool:
37
+ """Validate the X-Hub-Signature-256 HMAC header."""
38
+ if not signature_header or not signature_header.startswith("sha256="):
39
+ return False
40
+ expected = (
41
+ "sha256="
42
+ + hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
43
+ )
44
+ return hmac.compare_digest(expected, signature_header)
45
+
46
+
47
+ def _build_app() -> "FastAPI": # type: ignore[name-defined]
48
+ """Construct and return the FastAPI application instance."""
49
+ if not HAS_FASTAPI:
50
+ raise RuntimeError(
51
+ "FastAPI is not installed. Install it with: pip install fastapi uvicorn"
52
+ )
53
+
54
+ app = FastAPI(title="DevTorch GitHub Webhook Receiver")
55
+
56
+ @app.post("/webhook/github")
57
+ async def github_webhook(request: Request) -> Dict[str, Any]:
58
+ secret = os.environ.get(_WEBHOOK_SECRET_ENV, "")
59
+ if not secret:
60
+ logger.warning("DEVTORCH_GITHUB_WEBHOOK_SECRET is not set; ignoring event.")
61
+ return {"status": "webhook_secret_not_configured"}
62
+
63
+ payload = await request.body()
64
+ sig_header = request.headers.get("X-Hub-Signature-256", "")
65
+
66
+ if not _verify_signature(payload, sig_header, secret):
67
+ logger.warning("Invalid webhook signature received.")
68
+ return Response(content='{"error":"invalid signature"}', status_code=401, media_type="application/json") # type: ignore[return-value]
69
+
70
+ event_type = request.headers.get("X-GitHub-Event", "")
71
+
72
+ if event_type != "pull_request":
73
+ logger.debug("Ignoring non-PR event: %s", event_type)
74
+ return {"status": "ignored"}
75
+
76
+ import json as _json
77
+
78
+ event: Dict[str, Any] = _json.loads(payload)
79
+ action = event.get("action", "")
80
+
81
+ if action not in ("opened", "synchronize"):
82
+ logger.debug("Ignoring PR action: %s", action)
83
+ return {"status": "ignored"}
84
+
85
+ pr = event.get("pull_request", {})
86
+ body = pr.get("body") or ""
87
+ metadata = parse_pr_body(body)
88
+
89
+ logger.info(
90
+ "PR #%s (%s) DevTorch metadata: branch=%r commits=%d sensitivities=%d concepts=%d mcs=%s",
91
+ pr.get("number", "?"),
92
+ action,
93
+ metadata.branch,
94
+ len(metadata.commit_ids),
95
+ len(metadata.sensitivity_refs),
96
+ len(metadata.concept_refs),
97
+ metadata.mcs_score,
98
+ )
99
+
100
+ return {
101
+ "status": "ok",
102
+ "metadata": {
103
+ "branch": metadata.branch,
104
+ "commit_ids": metadata.commit_ids,
105
+ "sensitivity_refs": metadata.sensitivity_refs,
106
+ "concept_refs": metadata.concept_refs,
107
+ "mcs_score": metadata.mcs_score,
108
+ "has_devtorch_section": metadata.has_devtorch_section,
109
+ },
110
+ }
111
+
112
+ return app
113
+
114
+
115
+ # Module-level app instance — only created when FastAPI is available.
116
+ if HAS_FASTAPI:
117
+ app = _build_app()
118
+
119
+
120
+ # ---------------------------------------------------------------------------
121
+ # GitHubAppHandler — installation webhooks + access token issuance
122
+ # ---------------------------------------------------------------------------
123
+
124
+ _APP_KEY = ".devtorch/github_app.json"
125
+
126
+
127
+ class GitHubAppHandler:
128
+ """
129
+ Handles GitHub App installation webhooks and issues installation access tokens.
130
+
131
+ Requires env vars when get_installation_token() is called:
132
+ GITHUB_APP_ID — numeric GitHub App ID
133
+ GITHUB_APP_PRIVATE_KEY — PEM-format RSA private key (the .pem file contents)
134
+ """
135
+
136
+ def __init__(self, backend: Any, webhook_secret: bytes) -> None:
137
+ self._backend = backend
138
+ self._webhook_secret = webhook_secret
139
+
140
+ # ------------------------------------------------------------------
141
+ # Webhook handling
142
+ # ------------------------------------------------------------------
143
+
144
+ def handle_webhook(self, body: bytes, signature: str, event_type: str) -> dict:
145
+ if not self._verify_signature(body, signature):
146
+ return {"status": "forbidden", "reason": "invalid signature"}
147
+
148
+ try:
149
+ payload = json.loads(body)
150
+ except json.JSONDecodeError:
151
+ return {"status": "error", "reason": "invalid JSON"}
152
+
153
+ action = payload.get("action", "")
154
+ installation = payload.get("installation", {})
155
+ installation_id = installation.get("id")
156
+
157
+ if event_type == "installation":
158
+ if action == "created":
159
+ repos = [r["full_name"] for r in payload.get("repositories", [])]
160
+ self._store_installation(installation_id=installation_id, repositories=repos)
161
+ elif action == "deleted":
162
+ self._clear_installation()
163
+
164
+ elif event_type == "installation_repositories":
165
+ existing = self._load()
166
+ repos_added = [r["full_name"] for r in payload.get("repositories_added", [])]
167
+ repos_removed = {r["full_name"] for r in payload.get("repositories_removed", [])}
168
+ current = set(existing.get("repositories", []))
169
+ current = (current | set(repos_added)) - repos_removed
170
+ existing["repositories"] = sorted(current)
171
+ self._save(existing)
172
+
173
+ return {"status": "ok"}
174
+
175
+ def _verify_signature(self, body: bytes, signature: str) -> bool:
176
+ if not signature.startswith("sha256="):
177
+ return False
178
+ expected = "sha256=" + hmac.new(self._webhook_secret, body, hashlib.sha256).hexdigest()
179
+ return hmac.compare_digest(expected, signature)
180
+
181
+ def _load(self) -> dict:
182
+ try:
183
+ return json.loads(self._backend.read_bytes(_APP_KEY))
184
+ except (FileNotFoundError, json.JSONDecodeError):
185
+ return {}
186
+
187
+ def _save(self, data: dict) -> None:
188
+ self._backend.write_bytes(_APP_KEY, json.dumps(data, indent=2).encode())
189
+
190
+ def _store_installation(self, installation_id: int, repositories: list[str]) -> None:
191
+ data = self._load()
192
+ data["installation_id"] = installation_id
193
+ data["repositories"] = sorted(set(data.get("repositories", [])) | set(repositories))
194
+ self._save(data)
195
+
196
+ def _clear_installation(self) -> None:
197
+ self._save({})
198
+
199
+ # ------------------------------------------------------------------
200
+ # Installation token issuance
201
+ # ------------------------------------------------------------------
202
+
203
+ def get_installation_id(self) -> Optional[int]:
204
+ return self._load().get("installation_id")
205
+
206
+ def get_installation_token(self) -> Optional[str]:
207
+ """Issue a short-lived installation access token (valid 1 hour)."""
208
+ installation_id = self.get_installation_id()
209
+ if installation_id is None:
210
+ return None
211
+
212
+ jwt_token = self._make_jwt()
213
+ url = f"https://api.github.com/app/installations/{installation_id}/access_tokens"
214
+ req = urllib.request.Request(
215
+ url,
216
+ method="POST",
217
+ headers={
218
+ "Authorization": f"Bearer {jwt_token}",
219
+ "Accept": "application/vnd.github+json",
220
+ "User-Agent": "DevTorch-App",
221
+ "Content-Length": "0",
222
+ },
223
+ )
224
+ with urllib.request.urlopen(req, timeout=10) as resp:
225
+ data = json.loads(resp.read())
226
+ return data.get("token")
227
+
228
+ def _make_jwt(self) -> str:
229
+ """Create a signed JWT for the GitHub App (RS256, 10-minute expiry)."""
230
+ try:
231
+ import jwt as _jwt
232
+ except ImportError:
233
+ raise RuntimeError("PyJWT not installed. Install with: pip install 'devtorch-core[cloud]'")
234
+
235
+ app_id = os.environ["GITHUB_APP_ID"]
236
+ private_key = os.environ["GITHUB_APP_PRIVATE_KEY"]
237
+
238
+ now = int(time.time())
239
+ payload = {"iat": now - 60, "exp": now + 600, "iss": int(app_id)}
240
+ return _jwt.encode(payload, private_key, algorithm="RS256")
@@ -0,0 +1,113 @@
1
+ """
2
+ Builds GitHub PR comment markdown for DevTorch governance summaries.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass, field
8
+ from typing import List, Optional, Tuple
9
+
10
+ from .pr_parser import PRMetadata
11
+
12
+
13
+ @dataclass
14
+ class GovernanceSummary:
15
+ """Governance state to be surfaced in a PR comment."""
16
+
17
+ branch: str
18
+ node_state: str
19
+ top_concepts: List[Tuple[str, float]] = field(default_factory=list)
20
+ sensitivity_count: int = 0
21
+ high_disclosure_count: int = 0
22
+ mcs_score: Optional[float] = None
23
+ invariants_ok: bool = True
24
+ lock_status: str = "unlocked"
25
+
26
+
27
+ def build_pr_comment(
28
+ summary: GovernanceSummary,
29
+ pr_metadata: Optional[PRMetadata] = None,
30
+ ) -> str:
31
+ """
32
+ Build a GitHub PR comment in Markdown that summarises DevTorch governance state.
33
+
34
+ Args:
35
+ summary: Governance summary data (typically from devtorch doctor output).
36
+ pr_metadata: Optional parsed PR metadata; included in output when provided.
37
+
38
+ Returns:
39
+ A Markdown string suitable for posting as a GitHub PR comment.
40
+ """
41
+ lines: List[str] = []
42
+
43
+ # ── Header ────────────────────────────────────────────────────────────────
44
+ lines.append("## 🛡️ DevTorch Governance Report")
45
+ lines.append("")
46
+
47
+ # ── Main table ────────────────────────────────────────────────────────────
48
+ top_concepts_str = (
49
+ ", ".join(f"{name} ({conf:.2f})" for name, conf in summary.top_concepts)
50
+ if summary.top_concepts
51
+ else "—"
52
+ )
53
+
54
+ mcs_display = f"{summary.mcs_score:.2f}" if summary.mcs_score is not None else "N/A"
55
+ invariants_display = "✅ OK" if summary.invariants_ok else "❌ FAILED"
56
+
57
+ lines.append("| Field | Value |")
58
+ lines.append("|---|---|")
59
+ lines.append(f"| **Branch** | `{summary.branch}` |")
60
+ lines.append(f"| **Node State** | {summary.node_state} |")
61
+ lines.append(f"| **Top Concepts** | {top_concepts_str} |")
62
+ lines.append(f"| **Sensitivity Events** | {summary.sensitivity_count} |")
63
+ lines.append(f"| **MCS Score** | {mcs_display} |")
64
+ lines.append(f"| **Invariants** | {invariants_display} |")
65
+ lines.append(f"| **Lock Status** | {summary.lock_status} |")
66
+ lines.append("")
67
+
68
+ # ── Warnings ──────────────────────────────────────────────────────────────
69
+ if not summary.invariants_ok:
70
+ lines.append("> ⚠️ **Invariant check failed.** One or more DevTorch invariants")
71
+ lines.append("> (I1 commit-backed, I3 semantic grounding) did not pass.")
72
+ lines.append("> Review the invariant trace before merging.")
73
+ lines.append("")
74
+
75
+ if summary.high_disclosure_count > 0:
76
+ lines.append(
77
+ f"> 🔒 **{summary.high_disclosure_count} high-disclosure sensitivity"
78
+ f" event(s) detected.** Ensure downstream consumers comply with"
79
+ f" disclosure policy before merging."
80
+ )
81
+ lines.append("")
82
+
83
+ if summary.lock_status and summary.lock_status.lower() not in ("unlocked", ""):
84
+ lines.append(
85
+ f"> 🔐 **Consensus lock is active** (`{summary.lock_status}`). "
86
+ "Merge may be restricted by governance policy."
87
+ )
88
+ lines.append("")
89
+
90
+ # ── PR metadata section (optional) ────────────────────────────────────────
91
+ if pr_metadata is not None and pr_metadata.has_devtorch_section:
92
+ lines.append("### DevTorch PR Metadata")
93
+ lines.append("")
94
+ if pr_metadata.sensitivity_refs:
95
+ lines.append(
96
+ f"- **Sensitivity refs**: {', '.join(pr_metadata.sensitivity_refs)}"
97
+ )
98
+ if pr_metadata.concept_refs:
99
+ lines.append(f"- **Concept refs**: {', '.join(pr_metadata.concept_refs)}")
100
+ if pr_metadata.commit_ids:
101
+ short_ids = [c[:7] for c in pr_metadata.commit_ids]
102
+ lines.append(f"- **Commits**: {', '.join(short_ids)}")
103
+ if pr_metadata.mcs_score is not None:
104
+ lines.append(f"- **PR MCS Score**: {pr_metadata.mcs_score:.2f}")
105
+ lines.append("")
106
+
107
+ # ── Footer ────────────────────────────────────────────────────────────────
108
+ lines.append(
109
+ "---\n_Generated by DevTorch | "
110
+ "[docs](https://github.com/flotorch-ai/devtorch)_"
111
+ )
112
+
113
+ return "\n".join(lines)
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import json
5
+ from typing import Any, Optional
6
+
7
+
8
+ _PATS_KEY = ".devtorch/github_pats.json"
9
+
10
+
11
+ def _fernet_key(raw: bytes) -> bytes:
12
+ """Derive a 32-byte Fernet key from arbitrary bytes via SHA-256."""
13
+ import hashlib
14
+ digest = hashlib.sha256(raw).digest() # always 32 bytes
15
+ return base64.urlsafe_b64encode(digest)
16
+
17
+
18
+ class GitHubPATStore:
19
+ """
20
+ Stores GitHub Personal Access Tokens encrypted with Fernet (AES-128-CBC + HMAC).
21
+ PATs are stored at {prefix}/.devtorch/github_pats.json in the backend.
22
+
23
+ encryption_key: any bytes — hashed to 32 bytes via SHA-256 internally.
24
+ Generate a stable key with: python -c "import os; print(os.urandom(32).hex())"
25
+ Store as DEVTORCH_PAT_ENCRYPTION_KEY env var (hex string).
26
+ """
27
+
28
+ def __init__(self, backend: Any, encryption_key: bytes) -> None:
29
+ self._backend = backend
30
+ self._fernet_key = _fernet_key(encryption_key)
31
+
32
+ def _fernet(self):
33
+ from cryptography.fernet import Fernet
34
+ return Fernet(self._fernet_key)
35
+
36
+ def _load(self) -> dict:
37
+ try:
38
+ return json.loads(self._backend.read_bytes(_PATS_KEY))
39
+ except (FileNotFoundError, json.JSONDecodeError):
40
+ return {}
41
+
42
+ def _save(self, data: dict) -> None:
43
+ self._backend.write_bytes(_PATS_KEY, json.dumps(data, indent=2).encode())
44
+
45
+ def store_pat(self, repo_id: str, pat: str, github_repo: str) -> None:
46
+ """Encrypt and store the PAT for a repo_id → github_repo mapping."""
47
+ data = self._load()
48
+ encrypted = self._fernet().encrypt(pat.encode()).decode()
49
+ data[repo_id] = {
50
+ "github_repo": github_repo,
51
+ "pat_encrypted": encrypted,
52
+ }
53
+ self._save(data)
54
+
55
+ def get_pat(self, repo_id: str) -> Optional[dict[str, str]]:
56
+ """Return {"pat": "<decrypted>", "github_repo": "<owner/repo>"} or None."""
57
+ data = self._load()
58
+ entry = data.get(repo_id)
59
+ if not entry:
60
+ return None
61
+ try:
62
+ decrypted = self._fernet().decrypt(entry["pat_encrypted"].encode()).decode()
63
+ except Exception:
64
+ return None
65
+ return {"pat": decrypted, "github_repo": entry["github_repo"]}
66
+
67
+ def list_repos(self) -> list[str]:
68
+ """Return all repo_ids with stored PATs."""
69
+ return list(self._load().keys())
70
+
71
+ def delete_pat(self, repo_id: str) -> None:
72
+ """Remove the PAT for a repo_id."""
73
+ data = self._load()
74
+ if repo_id in data:
75
+ del data[repo_id]
76
+ self._save(data)
@@ -0,0 +1,82 @@
1
+ """
2
+ Parses GitHub PR bodies for DevTorch governance metadata.
3
+ Extracts structured data without requiring a .GCC/ directory.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ from dataclasses import dataclass, field
10
+ from typing import List, Optional
11
+
12
+
13
+ @dataclass
14
+ class PRMetadata:
15
+ """Structured DevTorch governance metadata extracted from a PR body."""
16
+
17
+ branch: str = ""
18
+ commit_ids: List[str] = field(default_factory=list)
19
+ sensitivity_refs: List[str] = field(default_factory=list)
20
+ concept_refs: List[str] = field(default_factory=list)
21
+ mcs_score: Optional[float] = None
22
+ has_devtorch_section: bool = False
23
+
24
+
25
+ # Patterns
26
+ _BRANCH_PATTERN = re.compile(r"Branch:\s*`([^`]+)`")
27
+ _COMMIT_PATTERN = re.compile(r"\b([0-9a-f]{40})\b", re.IGNORECASE)
28
+ _SENSITIVITY_PATTERN = re.compile(r"\[SENSITIVITY:([^\]]+)\]")
29
+ _CONCEPT_PATTERN = re.compile(r"\[CONCEPT:([^\]]+)\]")
30
+ _MCS_PATTERN = re.compile(r"MCS:\s*([\d]+\.[\d]+)")
31
+ _DEVTORCH_HEADING_PATTERN = re.compile(r"^#{2,3}\s+DevTorch\b", re.MULTILINE)
32
+
33
+
34
+ def parse_pr_body(body: str) -> PRMetadata:
35
+ """
36
+ Parse a GitHub PR body string and extract DevTorch governance metadata.
37
+
38
+ Args:
39
+ body: The full text of the PR description/body.
40
+
41
+ Returns:
42
+ A PRMetadata instance populated from whatever was found in the body.
43
+ Fields default to empty lists, None, or False when not found.
44
+ """
45
+ if not body:
46
+ return PRMetadata()
47
+
48
+ # Branch
49
+ branch = ""
50
+ branch_match = _BRANCH_PATTERN.search(body)
51
+ if branch_match:
52
+ branch = branch_match.group(1)
53
+
54
+ # Commit IDs (40-char lowercase hex strings)
55
+ commit_ids = list(dict.fromkeys(m.lower() for m in _COMMIT_PATTERN.findall(body)))
56
+
57
+ # Sensitivity refs
58
+ sensitivity_refs = list(dict.fromkeys(_SENSITIVITY_PATTERN.findall(body)))
59
+
60
+ # Concept refs
61
+ concept_refs = list(dict.fromkeys(_CONCEPT_PATTERN.findall(body)))
62
+
63
+ # MCS score
64
+ mcs_score: Optional[float] = None
65
+ mcs_match = _MCS_PATTERN.search(body)
66
+ if mcs_match:
67
+ try:
68
+ mcs_score = float(mcs_match.group(1))
69
+ except ValueError:
70
+ mcs_score = None
71
+
72
+ # DevTorch section heading
73
+ has_devtorch_section = bool(_DEVTORCH_HEADING_PATTERN.search(body))
74
+
75
+ return PRMetadata(
76
+ branch=branch,
77
+ commit_ids=commit_ids,
78
+ sensitivity_refs=sensitivity_refs,
79
+ concept_refs=concept_refs,
80
+ mcs_score=mcs_score,
81
+ has_devtorch_section=has_devtorch_section,
82
+ )