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,119 @@
1
+ """
2
+ Markdown and JSON rendering for ObservabilityReport.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ from typing import TYPE_CHECKING
9
+
10
+ from .report import ObservabilityReport, report_to_dict
11
+
12
+ if TYPE_CHECKING:
13
+ pass
14
+
15
+
16
+ def format_report_markdown(report: ObservabilityReport) -> str:
17
+ """
18
+ Render an ObservabilityReport as a Markdown string.
19
+
20
+ Sections:
21
+ # DevTorch Governance Report
22
+ ## Summary
23
+ ## Coordination Vector (Top 5 Θ)
24
+ ## Capability & Variance
25
+ ## Privacy Budget (RDP)
26
+ """
27
+ lines: list[str] = []
28
+
29
+ # ---------- Header ----------
30
+ lines.append("# DevTorch Governance Report")
31
+ lines.append(
32
+ f"Generated: {report.generated_at} | "
33
+ f"Branch: {report.branch} | "
34
+ f"State: {report.node_state.upper()}"
35
+ )
36
+ lines.append("")
37
+
38
+ # ---------- Summary table ----------
39
+ lines.append("## Summary")
40
+ lines.append("")
41
+ lines.append("| Metric | Value |")
42
+ lines.append("|--------|-------|")
43
+ lines.append(f"| Commits | {report.commit_count} |")
44
+ lines.append(f"| Sensitivity Events | {report.sensitivity_event_count} |")
45
+ lines.append(f"| High Disclosure | {report.high_disclosure_count} |")
46
+ lines.append(f"| Branches | {report.branch_count} |")
47
+ lines.append(f"| SIS Quarantines | {report.sis_quarantine_count} |")
48
+ lines.append(f"| GCC Version | {report.gcc_version} |")
49
+ lines.append("")
50
+
51
+ # ---------- Period ----------
52
+ if report.period is not None:
53
+ lines.append(f"**Period:** {report.period.period_label}")
54
+ if report.period.start:
55
+ lines.append(f" From: {report.period.start}")
56
+ if report.period.end:
57
+ lines.append(f" To: {report.period.end}")
58
+ lines.append("")
59
+
60
+ # ---------- Theta (Coordination Vector) ----------
61
+ lines.append("## Coordination Vector (Top 5 Θ)")
62
+ lines.append("")
63
+ if report.theta_top_concepts:
64
+ lines.append("| Concept | Confidence |")
65
+ lines.append("|---------|------------|")
66
+ for entry in report.theta_top_concepts:
67
+ concept = entry.get("concept", "")
68
+ conf = entry.get("mean_confidence", 0.0)
69
+ lines.append(f"| {concept} | {conf:.4f} |")
70
+ else:
71
+ lines.append("_No coordination vector data available._")
72
+ lines.append("")
73
+
74
+ # ---------- Capability & Variance ----------
75
+ lines.append("## Capability & Variance")
76
+ lines.append("")
77
+
78
+ cap = report.capability_state
79
+ textual_allowed = cap.get("textual_mode_allowed", False)
80
+ lipschitz = cap.get("lipschitz_bound")
81
+ lipschitz_str = f"{lipschitz:.4f}" if lipschitz is not None else "N/A"
82
+ lines.append(f"- **Textual Mode Allowed:** {'Yes' if textual_allowed else 'No'}")
83
+ lines.append(f"- **Lipschitz Bound (L̂):** {lipschitz_str}")
84
+ lines.append("")
85
+
86
+ var = report.variance_state
87
+ f_max_val = var.get("f_max")
88
+ f_max_str = str(f_max_val) if f_max_val is not None else "N/A"
89
+ det_mode = var.get("deterministic_mode", False)
90
+ alert_count = var.get("alert_count", 0)
91
+ lines.append(f"- **f_max:** {f_max_str}")
92
+ lines.append(f"- **Variance Alert Count:** {alert_count}")
93
+ lines.append(f"- **Deterministic Mode:** {'Yes' if det_mode else 'No'}")
94
+ lines.append("")
95
+
96
+ # ---------- Privacy Budget (RDP) ----------
97
+ lines.append("## Privacy Budget (RDP)")
98
+ lines.append("")
99
+ rdp = report.rdp_budget
100
+ eps_used = float(rdp.get("epsilon_used", 0.0))
101
+ eps_max = float(rdp.get("epsilon_max", 1.0))
102
+ read_only = bool(rdp.get("read_only", False))
103
+
104
+ if eps_max > 0:
105
+ pct = (eps_used / eps_max) * 100.0
106
+ pct_str = f"{pct:.1f}%"
107
+ else:
108
+ pct_str = "N/A"
109
+
110
+ lines.append(f"ε used: {eps_used:.4f} / {eps_max:.4f} ({pct_str})")
111
+ lines.append(f"- **Read-only mode:** {'Yes' if read_only else 'No'}")
112
+ lines.append("")
113
+
114
+ return "\n".join(lines)
115
+
116
+
117
+ def format_report_json(report: ObservabilityReport) -> str:
118
+ """Render an ObservabilityReport as a formatted JSON string."""
119
+ return json.dumps(report_to_dict(report), indent=2)
@@ -0,0 +1,264 @@
1
+ """
2
+ Structured observability report builder.
3
+
4
+ Reads all .GCC/ state and produces a machine-readable + human-readable
5
+ governance snapshot. Never reads prompts, code content, or raw LLM responses.
6
+ Only reads governance metadata: commits, sensitivities (counts), theta,
7
+ capabilities, variance state, RDP budget, branch refs.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import dataclasses
13
+ import datetime as _dt
14
+ import json
15
+ from pathlib import Path
16
+ from typing import Any, Dict, List, Optional
17
+
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Dataclasses
21
+ # ---------------------------------------------------------------------------
22
+
23
+ @dataclasses.dataclass
24
+ class ReportTimeRange:
25
+ """Optional time window for filtering report data."""
26
+ start: str # ISO timestamp or ""
27
+ end: str # ISO timestamp or ""
28
+ period_label: str # e.g. "last 7 days"
29
+
30
+
31
+ @dataclasses.dataclass
32
+ class ObservabilityReport:
33
+ """Full governance snapshot built from .GCC/ metadata only."""
34
+ generated_at: str
35
+ gcc_version: str
36
+ node_state: str
37
+ branch: str
38
+ commit_count: int
39
+ sensitivity_event_count: int
40
+ high_disclosure_count: int
41
+ theta_top_concepts: List[Dict[str, Any]] # [{concept: str, mean_confidence: float}]
42
+ capability_state: Dict[str, Any] # {textual_mode_allowed: bool, lipschitz_bound: float | None}
43
+ variance_state: Dict[str, Any] # {f_max: float | None, alert_count: int, deterministic_mode: bool}
44
+ rdp_budget: Dict[str, Any] # {epsilon_used: float, epsilon_max: float, read_only: bool}
45
+ branch_count: int
46
+ sis_quarantine_count: int
47
+ period: Optional[ReportTimeRange] = None
48
+
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # Report builder
52
+ # ---------------------------------------------------------------------------
53
+
54
+ def build_report(gcc_repo: Any, period: Optional[ReportTimeRange] = None) -> ObservabilityReport:
55
+ """
56
+ Build an ObservabilityReport from a GCCRepository instance.
57
+
58
+ Each section is guarded by try/except so a broken or missing sub-store
59
+ never prevents the rest of the report from being generated.
60
+
61
+ Never reads: prompt content, commit message bodies, code, sensitivity
62
+ signal text, or any raw LLM response content.
63
+ """
64
+ generated_at = _dt.datetime.now(tz=_dt.timezone.utc).isoformat()
65
+
66
+ # --- gcc_version ---
67
+ try:
68
+ version_path = gcc_repo.gcc_dir / "VERSION"
69
+ gcc_version = version_path.read_text(encoding="utf-8").strip() if version_path.exists() else ""
70
+ except Exception:
71
+ gcc_version = ""
72
+
73
+ # --- node_state ---
74
+ try:
75
+ ns_path = gcc_repo.gcc_dir / "NODE_STATE"
76
+ if ns_path.exists():
77
+ ns_data = json.loads(ns_path.read_text(encoding="utf-8"))
78
+ node_state = str(ns_data.get("mode", "unknown"))
79
+ else:
80
+ node_state = "unknown"
81
+ except Exception:
82
+ node_state = "unknown"
83
+
84
+ # --- current branch ---
85
+ try:
86
+ head_path = gcc_repo.gcc_dir / "refs" / "HEAD"
87
+ branch = head_path.read_text(encoding="utf-8").strip() if head_path.exists() else "unknown"
88
+ except Exception:
89
+ branch = "unknown"
90
+
91
+ # --- commit_count ---
92
+ try:
93
+ commits_dir = gcc_repo.gcc_dir / "commits"
94
+ if commits_dir.exists():
95
+ commit_count = len(list(commits_dir.glob("*.json")))
96
+ else:
97
+ commit_count = 0
98
+ except Exception:
99
+ commit_count = 0
100
+
101
+ # --- sensitivity counts (metadata only — no signal text) ---
102
+ sensitivity_event_count = 0
103
+ high_disclosure_count = 0
104
+ try:
105
+ sens_path = gcc_repo.gcc_dir / "sensitivities" / "events.jsonl"
106
+ if sens_path.exists():
107
+ with sens_path.open("r", encoding="utf-8") as f:
108
+ for line in f:
109
+ line = line.strip()
110
+ if not line:
111
+ continue
112
+ try:
113
+ ev = json.loads(line)
114
+ sensitivity_event_count += 1
115
+ if ev.get("disclosure_level") == "PRIVATE":
116
+ high_disclosure_count += 1
117
+ except (json.JSONDecodeError, KeyError):
118
+ continue
119
+ except Exception:
120
+ pass
121
+
122
+ # --- theta top concepts (metadata only — no content) ---
123
+ theta_top_concepts: List[Dict[str, Any]] = []
124
+ try:
125
+ theta_path = gcc_repo.gcc_dir / "theta.json"
126
+ if theta_path.exists():
127
+ theta_data = json.loads(theta_path.read_text(encoding="utf-8"))
128
+ cv: dict = theta_data.get("coordination_vector", {})
129
+ # Sort by mean_confidence descending, take top 5
130
+ sorted_concepts = sorted(
131
+ cv.items(),
132
+ key=lambda kv: float(kv[1].get("mean_confidence", 0.0)),
133
+ reverse=True,
134
+ )
135
+ for concept, entry in sorted_concepts[:5]:
136
+ theta_top_concepts.append({
137
+ "concept": str(concept),
138
+ "mean_confidence": float(entry.get("mean_confidence", 0.0)),
139
+ })
140
+ except Exception:
141
+ pass
142
+
143
+ # --- capability_state (A1 gate metadata — no calibration data body) ---
144
+ capability_state: Dict[str, Any] = {"textual_mode_allowed": False, "lipschitz_bound": None}
145
+ try:
146
+ omega_path = gcc_repo.gcc_dir / "capabilities" / "omega.json"
147
+ if omega_path.exists():
148
+ omega_data = json.loads(omega_path.read_text(encoding="utf-8"))
149
+ schema = omega_data.get("schema", "")
150
+ if schema == "omega-v2.1":
151
+ capability_state = {
152
+ "textual_mode_allowed": bool(omega_data.get("pst_status") == "passed"),
153
+ "lipschitz_bound": omega_data.get("lipschitz_bound"),
154
+ }
155
+ # else: stub format — leave defaults
156
+ except Exception:
157
+ pass
158
+
159
+ # --- variance_state ---
160
+ variance_state: Dict[str, Any] = {"f_max": None, "alert_count": 0, "deterministic_mode": False}
161
+ try:
162
+ live_path = gcc_repo.gcc_dir / "variance" / "live_state.json"
163
+ if live_path.exists():
164
+ live_data = json.loads(live_path.read_text(encoding="utf-8"))
165
+ variance_state = {
166
+ "f_max": live_data.get("last_f_max"),
167
+ "alert_count": _count_events_of_type(gcc_repo.gcc_dir, "VARIANCE_ALERT"),
168
+ "deterministic_mode": bool(live_data.get("deterministic_mode_forced", False)),
169
+ }
170
+ else:
171
+ variance_state["alert_count"] = _count_events_of_type(gcc_repo.gcc_dir, "VARIANCE_ALERT")
172
+ except Exception:
173
+ pass
174
+
175
+ # --- rdp_budget ---
176
+ rdp_budget: Dict[str, Any] = {"epsilon_used": 0.0, "epsilon_max": 1.0, "read_only": False}
177
+ try:
178
+ rdp_path = gcc_repo.gcc_dir / "rdp" / "rdp_state.json"
179
+ if rdp_path.exists():
180
+ rdp_data = json.loads(rdp_path.read_text(encoding="utf-8"))
181
+ rdp_budget = {
182
+ "epsilon_used": float(rdp_data.get("epsilon_spent", 0.0)),
183
+ "epsilon_max": float(rdp_data.get("epsilon_budget", 1.0)),
184
+ "read_only": bool(rdp_data.get("read_only", False)),
185
+ }
186
+ except Exception:
187
+ pass
188
+
189
+ # --- branch_count ---
190
+ try:
191
+ branches_dir = gcc_repo.gcc_dir / "refs" / "branches"
192
+ if branches_dir.exists():
193
+ branch_count = len([f for f in branches_dir.iterdir() if f.is_file()])
194
+ else:
195
+ branch_count = 0
196
+ except Exception:
197
+ branch_count = 0
198
+
199
+ # --- sis_quarantine_count ---
200
+ sis_quarantine_count = 0
201
+ try:
202
+ q_path = gcc_repo.gcc_dir / "sis" / "quarantine_events.jsonl"
203
+ if q_path.exists():
204
+ with q_path.open("r", encoding="utf-8") as f:
205
+ for line in f:
206
+ if line.strip():
207
+ sis_quarantine_count += 1
208
+ except Exception:
209
+ pass
210
+
211
+ return ObservabilityReport(
212
+ generated_at=generated_at,
213
+ gcc_version=gcc_version,
214
+ node_state=node_state,
215
+ branch=branch,
216
+ commit_count=commit_count,
217
+ sensitivity_event_count=sensitivity_event_count,
218
+ high_disclosure_count=high_disclosure_count,
219
+ theta_top_concepts=theta_top_concepts,
220
+ capability_state=capability_state,
221
+ variance_state=variance_state,
222
+ rdp_budget=rdp_budget,
223
+ branch_count=branch_count,
224
+ sis_quarantine_count=sis_quarantine_count,
225
+ period=period,
226
+ )
227
+
228
+
229
+ # ---------------------------------------------------------------------------
230
+ # Serialisation
231
+ # ---------------------------------------------------------------------------
232
+
233
+ def report_to_dict(report: ObservabilityReport) -> dict:
234
+ """Convert ObservabilityReport to a JSON-serializable dict."""
235
+ d = dataclasses.asdict(report)
236
+ # period is a nested dataclass — already handled by asdict; may be None
237
+ return d
238
+
239
+
240
+ # ---------------------------------------------------------------------------
241
+ # Internal helpers
242
+ # ---------------------------------------------------------------------------
243
+
244
+ def _count_events_of_type(gcc_dir: Path, event_type: str) -> int:
245
+ """Count events of a specific type in the event log. Metadata only."""
246
+ count = 0
247
+ try:
248
+ event_log = gcc_dir / "events.log.jsonl"
249
+ if not event_log.exists():
250
+ return 0
251
+ with event_log.open("r", encoding="utf-8") as f:
252
+ for line in f:
253
+ line = line.strip()
254
+ if not line:
255
+ continue
256
+ try:
257
+ obj = json.loads(line)
258
+ if obj.get("event_type") == event_type:
259
+ count += 1
260
+ except json.JSONDecodeError:
261
+ continue
262
+ except Exception:
263
+ pass
264
+ return count
@@ -0,0 +1,147 @@
1
+ """
2
+ ServiceNow integration for DevTorch change management.
3
+
4
+ Creates Change Request records in ServiceNow from observability reports.
5
+ Only includes metadata: DHS score, audit summary, branch, counts, and a link
6
+ placeholder. NEVER includes: prompts, code, reasoning tokens, commit message
7
+ bodies, sensitivity signal text, or LLM response content.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import base64
13
+ import dataclasses
14
+ import json
15
+ import os
16
+ import urllib.request
17
+ import urllib.error
18
+ from typing import Optional
19
+
20
+ from .report import ObservabilityReport
21
+ from .webhook import WebhookResult
22
+
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Dataclasses
26
+ # ---------------------------------------------------------------------------
27
+
28
+ @dataclasses.dataclass
29
+ class ServiceNowConfig:
30
+ """Configuration for ServiceNow Change Request creation."""
31
+ instance_url: str
32
+ username: str
33
+ password: str
34
+ timeout_seconds: int = 10
35
+ enabled: bool = True
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Config loader
40
+ # ---------------------------------------------------------------------------
41
+
42
+ def load_servicenow_config() -> Optional[ServiceNowConfig]:
43
+ """
44
+ Load ServiceNow configuration from environment variables.
45
+
46
+ Environment variables:
47
+ - DEVTORCH_SERVICENOW_INSTANCE (required)
48
+ - DEVTORCH_SERVICENOW_USERNAME (required)
49
+ - DEVTORCH_SERVICENOW_PASSWORD (required)
50
+
51
+ Returns None if required variables are missing.
52
+ Never raises.
53
+ """
54
+ try:
55
+ instance_url = os.environ.get("DEVTORCH_SERVICENOW_INSTANCE", "").strip()
56
+ username = os.environ.get("DEVTORCH_SERVICENOW_USERNAME", "").strip()
57
+ password = os.environ.get("DEVTORCH_SERVICENOW_PASSWORD", "").strip()
58
+ if not instance_url or not username or not password:
59
+ return None
60
+ return ServiceNowConfig(
61
+ instance_url=instance_url,
62
+ username=username,
63
+ password=password,
64
+ )
65
+ except Exception:
66
+ return None
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # Change record builder
71
+ # ---------------------------------------------------------------------------
72
+
73
+ def build_change_record(report: ObservabilityReport, developer_id: str) -> dict:
74
+ """
75
+ Build a ServiceNow Change Request record from an ObservabilityReport.
76
+
77
+ The record includes metadata only: DHS score, audit summary, branch, and
78
+ a link placeholder. No prompt, code, or reasoning content is included.
79
+ """
80
+ sensitivity_total = max(report.sensitivity_event_count, 1)
81
+ dhs_score = 1.0 - (report.high_disclosure_count / sensitivity_total)
82
+
83
+ # Generate a short, human-readable audit summary.
84
+ audit_summary = (
85
+ f"DevTorch governance report for branch {report.branch}. "
86
+ f"Commits: {report.commit_count}, sensitivity events: {report.sensitivity_event_count}, "
87
+ f"high disclosure: {report.high_disclosure_count}, quarantines: {report.sis_quarantine_count}. "
88
+ f"DHS score: {dhs_score:.4f}."
89
+ )
90
+
91
+ audit_url = f"https://audit.devtorch.example/{developer_id}/{report.branch}"
92
+
93
+ return {
94
+ "description": audit_summary,
95
+ "short_description": f"DevTorch governance review: {report.branch}",
96
+ "u_devtorch_dhs": round(dhs_score, 4),
97
+ "u_devtorch_audit_url": audit_url,
98
+ "requested_by": developer_id,
99
+ "category": "Software",
100
+ "priority": "3",
101
+ }
102
+
103
+
104
+ # ---------------------------------------------------------------------------
105
+ # Sender
106
+ # ---------------------------------------------------------------------------
107
+
108
+ def create_servicenow_change(record: dict, config: ServiceNowConfig) -> WebhookResult:
109
+ """
110
+ POST a Change Request record to ServiceNow REST API.
111
+
112
+ Endpoint: /api/now/table/change_request
113
+ Auth: Basic auth with config.username / config.password
114
+
115
+ Uses urllib.request — no external dependencies.
116
+ Never raises — returns WebhookResult with success=False on any error.
117
+ """
118
+ if not config.enabled:
119
+ return WebhookResult(success=False, status_code=0, error="ServiceNow export is disabled")
120
+
121
+ try:
122
+ base_url = config.instance_url.rstrip("/")
123
+ url = f"{base_url}/api/now/table/change_request"
124
+
125
+ body_bytes = json.dumps(record).encode("utf-8")
126
+ credentials = base64.b64encode(f"{config.username}:{config.password}".encode("utf-8")).decode("utf-8")
127
+
128
+ req = urllib.request.Request(
129
+ url=url,
130
+ data=body_bytes,
131
+ method="POST",
132
+ headers={
133
+ "Authorization": f"Basic {credentials}",
134
+ "Content-Type": "application/json",
135
+ },
136
+ )
137
+
138
+ with urllib.request.urlopen(req, timeout=config.timeout_seconds) as resp:
139
+ status_code = resp.status
140
+ return WebhookResult(success=True, status_code=status_code, error="")
141
+
142
+ except urllib.error.HTTPError as exc:
143
+ return WebhookResult(success=False, status_code=exc.code, error=str(exc))
144
+ except urllib.error.URLError as exc:
145
+ return WebhookResult(success=False, status_code=0, error=str(exc))
146
+ except Exception as exc:
147
+ return WebhookResult(success=False, status_code=0, error=str(exc))