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