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,81 @@
1
+ """
2
+ Session metrics writer/reader for DevTorch.
3
+
4
+ Session metrics capture per-session LLM usage signals.
5
+ Stored at .GCC/metrics/session/<session_id>.json.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+
15
+ _SESSION_DIR = "metrics/session"
16
+
17
+
18
+ def write_session_metrics(gcc_dir: Path, session_id: str, data: dict) -> Path:
19
+ """
20
+ Write session metrics to .GCC/metrics/session/<session_id>.json.
21
+
22
+ Expected data fields:
23
+ tokens_used — int, total tokens consumed this session
24
+ tokens_saved — int, estimated savings (default: tokens_used × 0.3)
25
+ coverage — float, ratio of committed decisions to total tool calls
26
+ confidence_distribution — dict mapping tier name → count
27
+ cold_start_ms — float, cold-start latency in milliseconds
28
+
29
+ Missing optional fields are computed or defaulted. Returns the path written.
30
+ """
31
+ gcc_dir = Path(gcc_dir)
32
+ session_dir = gcc_dir / _SESSION_DIR
33
+ session_dir.mkdir(parents=True, exist_ok=True)
34
+
35
+ tokens_used = data.get("tokens_used", 0)
36
+ tokens_saved = data.get("tokens_saved", int(tokens_used * 0.3))
37
+
38
+ full_history = data.get("full_history_tokens", 0)
39
+ bundle = data.get("bundle_tokens", 0)
40
+ latency_speedup = data.get("latency_speedup_factor", 0.0)
41
+ if latency_speedup == 0.0 and bundle > 0:
42
+ latency_speedup = round(full_history / bundle, 4)
43
+
44
+ record = {
45
+ "session_id": session_id,
46
+ "tokens_used": tokens_used,
47
+ "tokens_saved": tokens_saved,
48
+ "bundle_tokens": bundle,
49
+ "full_history_tokens": full_history,
50
+ "latency_speedup_factor": latency_speedup,
51
+ "developer_id": data.get("developer_id"),
52
+ "coverage": data.get("coverage", 0.0),
53
+ "confidence_distribution": data.get("confidence_distribution", {}),
54
+ "cold_start_ms": data.get("cold_start_ms", 0.0),
55
+ }
56
+ if "created_at" in data:
57
+ record["created_at"] = data["created_at"]
58
+
59
+ out_path = session_dir / f"{session_id}.json"
60
+ out_path.write_text(json.dumps(record, indent=2, sort_keys=True), encoding="utf-8")
61
+ return out_path
62
+
63
+
64
+ def read_session_metrics(gcc_dir: Path, session_id: str) -> Optional[dict]:
65
+ """
66
+ Read session metrics for *session_id*.
67
+
68
+ Returns the dict if found, None if missing or unreadable.
69
+ """
70
+ gcc_dir = Path(gcc_dir)
71
+ path = gcc_dir / _SESSION_DIR / f"{session_id}.json"
72
+ if not path.exists():
73
+ return None
74
+ try:
75
+ text = path.read_text(encoding="utf-8")
76
+ data = json.loads(text)
77
+ if not isinstance(data, dict):
78
+ return None
79
+ return data
80
+ except (json.JSONDecodeError, OSError):
81
+ return None
@@ -0,0 +1,117 @@
1
+ """
2
+ Shadow AI detector for DevTorch.
3
+
4
+ Compares git commit count to GCC COMMIT event count over the last 30 days.
5
+ A high unregistered commit ratio suggests developers are bypassing GCC — a
6
+ "shadow AI" pattern where AI-assisted commits skip governance logging.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import subprocess
13
+ from datetime import datetime, timezone, timedelta
14
+ from pathlib import Path
15
+
16
+
17
+ _EVENT_LOG = "events.log.jsonl"
18
+ _THIRTY_DAYS_SECONDS = 30 * 24 * 3600
19
+
20
+
21
+ def _utcnow() -> datetime:
22
+ return datetime.now(tz=timezone.utc)
23
+
24
+
25
+ def _git_commit_count(repo_root: Path) -> int:
26
+ """Count git commits in the last 30 days. Returns 0 on any error."""
27
+ try:
28
+ result = subprocess.run(
29
+ ["git", "-C", str(repo_root), "log", "--oneline", "--since=30.days.ago"],
30
+ capture_output=True,
31
+ text=True,
32
+ timeout=10,
33
+ )
34
+ if result.returncode != 0:
35
+ return 0
36
+ lines = [l for l in result.stdout.splitlines() if l.strip()]
37
+ return len(lines)
38
+ except Exception:
39
+ return 0
40
+
41
+
42
+ def _gcc_commit_count(gcc_dir: Path) -> int:
43
+ """Count COMMIT events in events.log.jsonl in the last 30 days. Returns 0 on any error."""
44
+ event_log = gcc_dir / _EVENT_LOG
45
+ if not event_log.exists():
46
+ return 0
47
+ cutoff = _utcnow() - timedelta(seconds=_THIRTY_DAYS_SECONDS)
48
+ count = 0
49
+ try:
50
+ with event_log.open(encoding="utf-8") as f:
51
+ for line in f:
52
+ line = line.strip()
53
+ if not line:
54
+ continue
55
+ try:
56
+ event = json.loads(line)
57
+ except json.JSONDecodeError:
58
+ continue
59
+ if event.get("event_type") != "COMMIT":
60
+ continue
61
+ ts_str = event.get("timestamp", "")
62
+ try:
63
+ # Parse ISO 8601 timestamp
64
+ ts = datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
65
+ if ts.tzinfo is None:
66
+ ts = ts.replace(tzinfo=timezone.utc)
67
+ if ts >= cutoff:
68
+ count += 1
69
+ except (ValueError, AttributeError):
70
+ # If we can't parse timestamp, include the event conservatively
71
+ count += 1
72
+ except OSError:
73
+ return 0
74
+ return count
75
+
76
+
77
+ def detect_shadow_ai(repo_root: Path) -> dict:
78
+ """
79
+ Compare git commit count to GCC COMMIT event count over the last 30 days.
80
+
81
+ Returns:
82
+ {
83
+ "git_commits": int — total git commits in last 30 days
84
+ "gcc_commits": int — GCC COMMIT events in last 30 days
85
+ "unregistered": int — git_commits - gcc_commits (floor 0)
86
+ "shadow_ratio": float — unregistered / git_commits (0.0 if no git commits)
87
+ "flagged": bool — True when unregistered > 3 and shadow_ratio > 0.3
88
+ }
89
+
90
+ All errors return safe defaults (0 counts, flagged=False).
91
+ """
92
+ try:
93
+ repo_root = Path(repo_root)
94
+ gcc_dir = repo_root / ".GCC"
95
+
96
+ git_commits = _git_commit_count(repo_root)
97
+ gcc_commits = _gcc_commit_count(gcc_dir)
98
+
99
+ unregistered = max(0, git_commits - gcc_commits)
100
+ shadow_ratio = unregistered / git_commits if git_commits > 0 else 0.0
101
+ flagged = unregistered > 3 and shadow_ratio > 0.3
102
+
103
+ return {
104
+ "git_commits": git_commits,
105
+ "gcc_commits": gcc_commits,
106
+ "unregistered": unregistered,
107
+ "shadow_ratio": shadow_ratio,
108
+ "flagged": flagged,
109
+ }
110
+ except Exception:
111
+ return {
112
+ "git_commits": 0,
113
+ "gcc_commits": 0,
114
+ "unregistered": 0,
115
+ "shadow_ratio": 0.0,
116
+ "flagged": False,
117
+ }
@@ -0,0 +1,243 @@
1
+ """
2
+ Sprint metrics writer/reader for DevTorch.
3
+
4
+ Sprint metrics capture per-sprint governance signals.
5
+ Stored at .GCC/metrics/sprint/<sprint_id>.json.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import datetime as _dt
11
+ import json
12
+ import math
13
+ from pathlib import Path
14
+ from typing import Any, Dict, List, Optional
15
+
16
+ from devtorch_core.metrics.mcs import compute_mcs, CollisionEvent
17
+
18
+
19
+ _SPRINT_DIR = "metrics/sprint"
20
+
21
+
22
+ def _read_events_log(gcc_dir: Path) -> List[Dict[str, Any]]:
23
+ """Read all events from events.log.jsonl; return empty list on any error."""
24
+ log_file = gcc_dir / "events.log.jsonl"
25
+ events: List[Dict[str, Any]] = []
26
+ if not log_file.exists():
27
+ return events
28
+ try:
29
+ with log_file.open("r", encoding="utf-8") as fh:
30
+ for line in fh:
31
+ line = line.strip()
32
+ if not line:
33
+ continue
34
+ try:
35
+ events.append(json.loads(line))
36
+ except json.JSONDecodeError:
37
+ continue
38
+ except OSError:
39
+ pass
40
+ return events
41
+
42
+
43
+ def _read_commit_count(gcc_dir: Path) -> int:
44
+ """Count commit files in .GCC/commits."""
45
+ commits_dir = gcc_dir / "commits"
46
+ if not commits_dir.exists():
47
+ return 0
48
+ return sum(1 for p in commits_dir.iterdir() if p.suffix == ".json")
49
+
50
+
51
+ def _read_branch_count(gcc_dir: Path) -> int:
52
+ """Count branch ref files in .GCC/refs/branches."""
53
+ branches_dir = gcc_dir / "refs" / "branches"
54
+ if not branches_dir.exists():
55
+ return 0
56
+ return sum(1 for p in branches_dir.iterdir() if p.is_file())
57
+
58
+
59
+ def _read_theta_collisions(gcc_dir: Path) -> List[CollisionEvent]:
60
+ """Detect collisions from theta.json where source_nodes diverge (std_dev > 0.2)."""
61
+ theta_file = gcc_dir / "theta.json"
62
+ if not theta_file.exists():
63
+ return []
64
+ try:
65
+ data = json.loads(theta_file.read_text(encoding="utf-8"))
66
+ except (json.JSONDecodeError, OSError):
67
+ return []
68
+
69
+ cv: Dict[str, Any] = data.get("coordination_vector", {})
70
+ collisions: List[CollisionEvent] = []
71
+ for concept, info in cv.items():
72
+ source_nodes: List[Dict[str, Any]] = info.get("source_nodes", [])
73
+ if len(source_nodes) < 2:
74
+ continue
75
+ confidences = [float(n.get("confidence", 0.0)) for n in source_nodes]
76
+ mean_c = sum(confidences) / len(confidences)
77
+ variance = sum((c - mean_c) ** 2 for c in confidences) / len(confidences)
78
+ std_dev = math.sqrt(variance)
79
+ if std_dev <= 0.2:
80
+ continue
81
+ severity = 0.8 if std_dev > 0.4 else 0.5
82
+ collisions.append(
83
+ CollisionEvent(
84
+ timestamp=info.get("last_updated", "") or "",
85
+ branch_a=str(source_nodes[0].get("branch", source_nodes[0].get("agent_id", "a"))),
86
+ branch_b=str(source_nodes[1].get("branch", source_nodes[1].get("agent_id", "b"))),
87
+ concept=concept,
88
+ severity=severity,
89
+ resolution_minutes=0.0,
90
+ )
91
+ )
92
+ return collisions
93
+
94
+
95
+ def _count_i3_violations(events: List[Dict[str, Any]]) -> int:
96
+ """Count I3 invariant violations, falling back to any invariant violation."""
97
+ i3 = sum(
98
+ 1
99
+ for ev in events
100
+ if ev.get("event_type", ev.get("type", "")) == "INVARIANT_VIOLATION"
101
+ and "I3" in str(ev.get("invariant", ev.get("detail", "")))
102
+ )
103
+ if i3 == 0:
104
+ i3 = sum(
105
+ 1
106
+ for ev in events
107
+ if ev.get("event_type", ev.get("type", "")) == "INVARIANT_VIOLATION"
108
+ )
109
+ return i3
110
+
111
+
112
+ def _decision_rates(events: List[Dict[str, Any]]) -> tuple[float, float]:
113
+ """Return (acceptance_rate, override_rate) from decision-like events."""
114
+ decision_types: set[str] = {"ACCEPT", "OVERRIDE", "HITL_REQUEST", "REJECT"}
115
+ decisions: List[Dict[str, Any]] = []
116
+ for ev in events:
117
+ etype = ev.get("event_type", ev.get("type", "")).upper()
118
+ if etype in decision_types or any(t in etype for t in decision_types):
119
+ decisions.append(ev)
120
+ if not decisions:
121
+ return 1.0, 0.0
122
+ accepted = sum(
123
+ 1 for d in decisions if "ACCEPT" in d.get("event_type", d.get("type", "")).upper()
124
+ )
125
+ overridden = sum(
126
+ 1 for d in decisions if "OVERRIDE" in d.get("event_type", d.get("type", "")).upper()
127
+ )
128
+ n = len(decisions)
129
+ return round(accepted / n, 4), round(overridden / n, 4)
130
+
131
+
132
+ def compute_sprint_metrics(gcc_dir: Path, sprint_id: Optional[str] = None) -> Dict[str, Any]:
133
+ """
134
+ Compute sprint metrics from the current .GCC/ state and persist them.
135
+
136
+ Metrics computed:
137
+ mcs_score — from theta.json source-node divergence
138
+ i3_violations — count of I3 invariant failures
139
+ collisions_prevented — count of collisions detected (proxy for prevented)
140
+ override_rate — fraction of overridden AI suggestions
141
+ acceptance_rate — fraction of accepted AI suggestions
142
+ branch_count — number of active branches
143
+ commit_count — number of commits
144
+
145
+ If *sprint_id* is None, a default id of the form "sprint_YYYY-MM-DD" is used.
146
+ Returns the metrics dict (not the Pydantic model).
147
+ """
148
+ gcc_dir = Path(gcc_dir)
149
+ events = _read_events_log(gcc_dir)
150
+ collisions = _read_theta_collisions(gcc_dir)
151
+
152
+ mcs_score = compute_mcs(collisions)
153
+ i3_violations = _count_i3_violations(events)
154
+ collisions_prevented = len(collisions) + sum(
155
+ 1 for ev in events if ev.get("event_type", ev.get("type", "")) == "VARIANCE_ALERT"
156
+ )
157
+ acceptance_rate, override_rate = _decision_rates(events)
158
+ branch_count = _read_branch_count(gcc_dir)
159
+ commit_count = _read_commit_count(gcc_dir)
160
+
161
+ if sprint_id is None:
162
+ sprint_id = f"sprint_{_dt.date.today().isoformat()}"
163
+
164
+ record = {
165
+ "sprint_id": sprint_id,
166
+ "mcs_score": mcs_score,
167
+ "i3_violations": i3_violations,
168
+ "collisions_prevented": collisions_prevented,
169
+ "override_rate": override_rate,
170
+ "acceptance_rate": acceptance_rate,
171
+ "branch_count": branch_count,
172
+ "commit_count": commit_count,
173
+ }
174
+
175
+ sprint_dir = gcc_dir / _SPRINT_DIR
176
+ sprint_dir.mkdir(parents=True, exist_ok=True)
177
+ out_path = sprint_dir / f"{sprint_id}.json"
178
+ out_path.write_text(json.dumps(record, indent=2, sort_keys=True), encoding="utf-8")
179
+ return record
180
+
181
+
182
+ def write_sprint_metrics(gcc_dir: Path, sprint_id: str, data: dict) -> Path:
183
+ """
184
+ Write sprint metrics to .GCC/metrics/sprint/<sprint_id>.json.
185
+
186
+ Expected data fields:
187
+ mcs_score — float [0,1]; computed via compute_mcs if not provided
188
+ i3_violations — int, count of I3 invariant failures this sprint
189
+ collisions_prevented — int, estimated collisions prevented by GCC
190
+ override_rate — float [0,1], fraction of AI suggestions overridden
191
+ acceptance_rate — float [0,1], fraction of AI suggestions accepted
192
+ branch_count — int, number of branches active this sprint
193
+ commit_count — int, number of commits this sprint
194
+
195
+ If mcs_score is not provided, attempts to compute it from an empty event list
196
+ (returns 0.0). Returns the path written.
197
+ """
198
+ gcc_dir = Path(gcc_dir)
199
+ sprint_dir = gcc_dir / _SPRINT_DIR
200
+ sprint_dir.mkdir(parents=True, exist_ok=True)
201
+
202
+ if "mcs_score" not in data:
203
+ try:
204
+ mcs_score = compute_mcs([])
205
+ except Exception:
206
+ mcs_score = 0.0
207
+ else:
208
+ mcs_score = float(data["mcs_score"])
209
+
210
+ record = {
211
+ "sprint_id": sprint_id,
212
+ "mcs_score": mcs_score,
213
+ "i3_violations": int(data.get("i3_violations", 0)),
214
+ "collisions_prevented": int(data.get("collisions_prevented", 0)),
215
+ "override_rate": float(data.get("override_rate", 0.0)),
216
+ "acceptance_rate": float(data.get("acceptance_rate", 0.0)),
217
+ "branch_count": int(data.get("branch_count", 0)),
218
+ "commit_count": int(data.get("commit_count", 0)),
219
+ }
220
+
221
+ out_path = sprint_dir / f"{sprint_id}.json"
222
+ out_path.write_text(json.dumps(record, indent=2, sort_keys=True), encoding="utf-8")
223
+ return out_path
224
+
225
+
226
+ def read_sprint_metrics(gcc_dir: Path, sprint_id: str) -> Optional[dict]:
227
+ """
228
+ Read sprint metrics for *sprint_id*.
229
+
230
+ Returns the dict if found, None if missing or unreadable.
231
+ """
232
+ gcc_dir = Path(gcc_dir)
233
+ path = gcc_dir / _SPRINT_DIR / f"{sprint_id}.json"
234
+ if not path.exists():
235
+ return None
236
+ try:
237
+ text = path.read_text(encoding="utf-8")
238
+ data = json.loads(text)
239
+ if not isinstance(data, dict):
240
+ return None
241
+ return data
242
+ except (json.JSONDecodeError, OSError):
243
+ return None
@@ -0,0 +1,78 @@
1
+ """
2
+ devtorch_core.observability — Structured governance reporting and enterprise metrics.
3
+
4
+ Public API:
5
+ - ObservabilityReport, build_report (report.py)
6
+ - format_report_markdown, format_report_json (formatter.py)
7
+ - WebhookPayload, send_webhook, send_metrics_if_configured (webhook.py)
8
+ """
9
+
10
+ from .report import (
11
+ ObservabilityReport,
12
+ ReportTimeRange,
13
+ build_report,
14
+ report_to_dict,
15
+ )
16
+ from .formatter import (
17
+ format_report_markdown,
18
+ format_report_json,
19
+ )
20
+ from .webhook import (
21
+ WebhookConfig,
22
+ WebhookPayload,
23
+ WebhookResult,
24
+ load_webhook_config,
25
+ build_webhook_payload,
26
+ send_webhook,
27
+ send_metrics_if_configured,
28
+ )
29
+ from .datadog import (
30
+ DatadogConfig,
31
+ load_datadog_config,
32
+ send_datadog_metrics,
33
+ )
34
+ from .splunk import (
35
+ SplunkConfig,
36
+ load_splunk_config,
37
+ send_to_splunk,
38
+ stream_events_log,
39
+ )
40
+ from .servicenow import (
41
+ ServiceNowConfig,
42
+ load_servicenow_config,
43
+ build_change_record,
44
+ create_servicenow_change,
45
+ )
46
+
47
+ __all__ = [
48
+ # report
49
+ "ObservabilityReport",
50
+ "ReportTimeRange",
51
+ "build_report",
52
+ "report_to_dict",
53
+ # formatter
54
+ "format_report_markdown",
55
+ "format_report_json",
56
+ # webhook
57
+ "WebhookConfig",
58
+ "WebhookPayload",
59
+ "WebhookResult",
60
+ "load_webhook_config",
61
+ "build_webhook_payload",
62
+ "send_webhook",
63
+ "send_metrics_if_configured",
64
+ # datadog
65
+ "DatadogConfig",
66
+ "load_datadog_config",
67
+ "send_datadog_metrics",
68
+ # splunk
69
+ "SplunkConfig",
70
+ "load_splunk_config",
71
+ "send_to_splunk",
72
+ "stream_events_log",
73
+ # servicenow
74
+ "ServiceNowConfig",
75
+ "load_servicenow_config",
76
+ "build_change_record",
77
+ "create_servicenow_change",
78
+ ]
@@ -0,0 +1,157 @@
1
+ """
2
+ Datadog metrics exporter for DevTorch observability reports.
3
+
4
+ Sends metrics-only time-series points to the Datadog `/api/v1/series` endpoint.
5
+ NEVER includes: prompts, code, reasoning tokens, commit message bodies,
6
+ sensitivity signal text, or any LLM response content.
7
+
8
+ Only sends: counts, scores, timestamps, and branch/state metadata.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import dataclasses
14
+ import json
15
+ import os
16
+ import time
17
+ import urllib.request
18
+ import urllib.error
19
+ from typing import Any, Dict, List, Optional
20
+
21
+ from .report import ObservabilityReport
22
+ from .webhook import WebhookResult
23
+
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Dataclasses
27
+ # ---------------------------------------------------------------------------
28
+
29
+ @dataclasses.dataclass
30
+ class DatadogConfig:
31
+ """Configuration for Datadog metrics export."""
32
+ api_key: str
33
+ site: str = "datadoghq.com"
34
+ timeout_seconds: int = 5
35
+ enabled: bool = True
36
+ prefix: str = "devtorch"
37
+
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # Config loader
41
+ # ---------------------------------------------------------------------------
42
+
43
+ def load_datadog_config() -> Optional[DatadogConfig]:
44
+ """
45
+ Load Datadog configuration from environment variables.
46
+
47
+ Environment variables:
48
+ - DEVTORCH_DATADOG_API_KEY (required)
49
+ - DEVTORCH_DATADOG_SITE
50
+ - DEVTORCH_DATADOG_TIMEOUT_SECONDS
51
+
52
+ Returns None if DEVTORCH_DATADOG_API_KEY is not set.
53
+ Never raises.
54
+ """
55
+ try:
56
+ api_key = os.environ.get("DEVTORCH_DATADOG_API_KEY", "").strip()
57
+ if not api_key:
58
+ return None
59
+ site = os.environ.get("DEVTORCH_DATADOG_SITE", "datadoghq.com").strip() or "datadoghq.com"
60
+ timeout_str = os.environ.get("DEVTORCH_DATADOG_TIMEOUT_SECONDS", "5").strip()
61
+ try:
62
+ timeout_seconds = int(timeout_str)
63
+ except (ValueError, TypeError):
64
+ timeout_seconds = 5
65
+ return DatadogConfig(
66
+ api_key=api_key,
67
+ site=site,
68
+ timeout_seconds=timeout_seconds,
69
+ )
70
+ except Exception:
71
+ return None
72
+
73
+
74
+ # ---------------------------------------------------------------------------
75
+ # Metrics builder
76
+ # ---------------------------------------------------------------------------
77
+
78
+ def _build_series_payload(report: ObservabilityReport) -> Dict[str, Any]:
79
+ """
80
+ Build a Datadog `/api/v1/series` payload from an ObservabilityReport.
81
+
82
+ Only includes metadata metrics: counts, scores, and timestamp fields.
83
+ """
84
+ timestamp = int(time.time())
85
+ prefix = "devtorch"
86
+
87
+ rdp = report.rdp_budget
88
+ rdp_epsilon_used = float(rdp.get("epsilon_used", 0.0))
89
+
90
+ # Disclosure health score: 1.0 when no high-disclosure events, lower otherwise.
91
+ sensitivity_total = max(report.sensitivity_event_count, 1)
92
+ dhs_score = 1.0 - (report.high_disclosure_count / sensitivity_total)
93
+
94
+ # Metadata capability score: simple binary signal from textual mode gating.
95
+ cap = report.capability_state
96
+ textual_allowed = bool(cap.get("textual_mode_allowed", False))
97
+ mcs_score = 1.0 if textual_allowed else 0.0
98
+
99
+ series: List[Dict[str, Any]] = [
100
+ {"metric": f"{prefix}.commit_count", "points": [[timestamp, report.commit_count]], "type": "gauge"},
101
+ {"metric": f"{prefix}.sensitivity_event_count", "points": [[timestamp, report.sensitivity_event_count]], "type": "gauge"},
102
+ {"metric": f"{prefix}.high_disclosure_count", "points": [[timestamp, report.high_disclosure_count]], "type": "gauge"},
103
+ {"metric": f"{prefix}.branch_count", "points": [[timestamp, report.branch_count]], "type": "gauge"},
104
+ {"metric": f"{prefix}.sis_quarantine_count", "points": [[timestamp, report.sis_quarantine_count]], "type": "gauge"},
105
+ {"metric": f"{prefix}.mcs_score", "points": [[timestamp, mcs_score]], "type": "gauge"},
106
+ {"metric": f"{prefix}.dhs_score", "points": [[timestamp, dhs_score]], "type": "gauge"},
107
+ {"metric": f"{prefix}.collision_count", "points": [[timestamp, 0]], "type": "gauge"},
108
+ {"metric": f"{prefix}.theta_concept_count", "points": [[timestamp, len(report.theta_top_concepts)]], "type": "gauge"},
109
+ {"metric": f"{prefix}.rdp_epsilon_used", "points": [[timestamp, rdp_epsilon_used]], "type": "gauge"},
110
+ ]
111
+
112
+ return {"series": series}
113
+
114
+
115
+ # ---------------------------------------------------------------------------
116
+ # Sender
117
+ # ---------------------------------------------------------------------------
118
+
119
+ def send_datadog_metrics(report: ObservabilityReport, config: DatadogConfig) -> WebhookResult:
120
+ """
121
+ POST an ObservabilityReport as Datadog time-series metrics.
122
+
123
+ Uses urllib.request — no external dependencies.
124
+ Never raises — returns WebhookResult with success=False on any error.
125
+
126
+ Headers sent:
127
+ DD-API-KEY: {api_key}
128
+ Content-Type: application/json
129
+ """
130
+ if not config.enabled:
131
+ return WebhookResult(success=False, status_code=0, error="Datadog export is disabled")
132
+
133
+ try:
134
+ body_dict = _build_series_payload(report)
135
+ body_bytes = json.dumps(body_dict).encode("utf-8")
136
+
137
+ url = f"https://api.{config.site}/api/v1/series"
138
+ req = urllib.request.Request(
139
+ url=url,
140
+ data=body_bytes,
141
+ method="POST",
142
+ headers={
143
+ "DD-API-KEY": config.api_key,
144
+ "Content-Type": "application/json",
145
+ },
146
+ )
147
+
148
+ with urllib.request.urlopen(req, timeout=config.timeout_seconds) as resp:
149
+ status_code = resp.status
150
+ return WebhookResult(success=True, status_code=status_code, error="")
151
+
152
+ except urllib.error.HTTPError as exc:
153
+ return WebhookResult(success=False, status_code=exc.code, error=str(exc))
154
+ except urllib.error.URLError as exc:
155
+ return WebhookResult(success=False, status_code=0, error=str(exc))
156
+ except Exception as exc:
157
+ return WebhookResult(success=False, status_code=0, error=str(exc))