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