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,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
devtorch_core.metrics — Metrics calibration and scoring for DevTorch.
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
- CalibrationDefaults / CalibrationStore / load_calibration (calibration.py)
|
|
6
|
+
- CollisionEvent / compute_mcs / mcs_to_label (mcs.py)
|
|
7
|
+
- DHSComponents / compute_dhs / dhs_to_label (dhs.py)
|
|
8
|
+
- ROIEstimate / estimate_context_switch_savings / estimate_rework_savings (roi.py)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .calibration import CalibrationDefaults, CalibrationStore, load_calibration
|
|
12
|
+
from .mcs import CollisionEvent, compute_mcs, mcs_to_label, estimate_rework_cost
|
|
13
|
+
from .dhs import DHSComponents, compute_dhs, dhs_to_label, compute_dhs_from_gcc
|
|
14
|
+
from .roi import ROIEstimate, estimate_context_switch_savings, estimate_rework_savings
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
# calibration
|
|
18
|
+
"CalibrationDefaults",
|
|
19
|
+
"CalibrationStore",
|
|
20
|
+
"load_calibration",
|
|
21
|
+
# MCS
|
|
22
|
+
"CollisionEvent",
|
|
23
|
+
"compute_mcs",
|
|
24
|
+
"mcs_to_label",
|
|
25
|
+
"estimate_rework_cost",
|
|
26
|
+
# DHS
|
|
27
|
+
"DHSComponents",
|
|
28
|
+
"compute_dhs",
|
|
29
|
+
"dhs_to_label",
|
|
30
|
+
"compute_dhs_from_gcc",
|
|
31
|
+
# ROI
|
|
32
|
+
"ROIEstimate",
|
|
33
|
+
"estimate_context_switch_savings",
|
|
34
|
+
"estimate_rework_savings",
|
|
35
|
+
]
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sprint 20 — Query-time aggregation for token savings and latency speedup.
|
|
3
|
+
|
|
4
|
+
All numbers are derived from .GCC/metrics/session/*.json files at query time.
|
|
5
|
+
No pre-aggregated files are produced; this keeps the numbers consistent.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from devtorch_core.projects import ProjectRegistry
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _read_session_files(session_dir: Path) -> List[dict]:
|
|
17
|
+
"""Read all valid session metric files from a directory."""
|
|
18
|
+
files: List[dict] = []
|
|
19
|
+
if not session_dir.exists():
|
|
20
|
+
return files
|
|
21
|
+
for path in session_dir.glob("*.json"):
|
|
22
|
+
try:
|
|
23
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
24
|
+
if isinstance(data, dict):
|
|
25
|
+
files.append(data)
|
|
26
|
+
except (json.JSONDecodeError, OSError):
|
|
27
|
+
continue
|
|
28
|
+
return files
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def aggregate_tokens(gcc_dir: Path) -> Dict[str, object]:
|
|
32
|
+
"""
|
|
33
|
+
Aggregate token usage across all session files in the repository.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
{
|
|
37
|
+
"total_tokens_used": int,
|
|
38
|
+
"total_tokens_saved": int,
|
|
39
|
+
"total_bundle_tokens": int,
|
|
40
|
+
"total_full_history_tokens": int,
|
|
41
|
+
"sessions_count": int,
|
|
42
|
+
"avg_tokens_used": float,
|
|
43
|
+
}
|
|
44
|
+
"""
|
|
45
|
+
sessions = _read_session_files(gcc_dir / "metrics" / "session")
|
|
46
|
+
total_used = sum(s.get("tokens_used", 0) for s in sessions)
|
|
47
|
+
total_saved = sum(s.get("tokens_saved", 0) for s in sessions)
|
|
48
|
+
total_bundle = sum(s.get("bundle_tokens", 0) for s in sessions)
|
|
49
|
+
total_full_history = sum(s.get("full_history_tokens", 0) for s in sessions)
|
|
50
|
+
count = len(sessions)
|
|
51
|
+
return {
|
|
52
|
+
"total_tokens_used": total_used,
|
|
53
|
+
"total_tokens_saved": total_saved,
|
|
54
|
+
"total_bundle_tokens": total_bundle,
|
|
55
|
+
"total_full_history_tokens": total_full_history,
|
|
56
|
+
"sessions_count": count,
|
|
57
|
+
"avg_tokens_used": total_used / count if count else 0.0,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def aggregate_speedup(gcc_dir: Path) -> Dict[str, object]:
|
|
62
|
+
"""
|
|
63
|
+
Aggregate latency speedup across all session files.
|
|
64
|
+
|
|
65
|
+
Speedup is computed as full_history_tokens / bundle_tokens. It is a proxy
|
|
66
|
+
for LLM call latency reduction and is always available from the session
|
|
67
|
+
files. If bundle_tokens is zero, speedup is reported as 1.0 (no reduction).
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
{
|
|
71
|
+
"latency_speedup_factor": float,
|
|
72
|
+
"weighted_avg_speedup": float,
|
|
73
|
+
"total_full_history_tokens": int,
|
|
74
|
+
"total_bundle_tokens": int,
|
|
75
|
+
"sessions_count": int,
|
|
76
|
+
}
|
|
77
|
+
"""
|
|
78
|
+
sessions = _read_session_files(gcc_dir / "metrics" / "session")
|
|
79
|
+
total_full = sum(s.get("full_history_tokens", 0) for s in sessions)
|
|
80
|
+
total_bundle = sum(s.get("bundle_tokens", 0) for s in sessions)
|
|
81
|
+
count = len(sessions)
|
|
82
|
+
|
|
83
|
+
if total_bundle > 0:
|
|
84
|
+
weighted_speedup = total_full / total_bundle
|
|
85
|
+
else:
|
|
86
|
+
weighted_speedup = 1.0
|
|
87
|
+
|
|
88
|
+
simple_speedups = []
|
|
89
|
+
for s in sessions:
|
|
90
|
+
full = s.get("full_history_tokens", 0)
|
|
91
|
+
bundle = s.get("bundle_tokens", 0)
|
|
92
|
+
if bundle > 0:
|
|
93
|
+
simple_speedups.append(full / bundle)
|
|
94
|
+
|
|
95
|
+
avg_speedup = sum(simple_speedups) / len(simple_speedups) if simple_speedups else 1.0
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
"latency_speedup_factor": weighted_speedup,
|
|
99
|
+
"weighted_avg_speedup": weighted_speedup,
|
|
100
|
+
"simple_avg_speedup": avg_speedup,
|
|
101
|
+
"total_full_history_tokens": total_full,
|
|
102
|
+
"total_bundle_tokens": total_bundle,
|
|
103
|
+
"sessions_count": count,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def aggregate_by_developer(gcc_dir: Path) -> Dict[str, dict]:
|
|
108
|
+
"""
|
|
109
|
+
Aggregate tokens/speedup per developer_id found in session files.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
{
|
|
113
|
+
developer_id: {
|
|
114
|
+
"tokens_used": int,
|
|
115
|
+
"tokens_saved": int,
|
|
116
|
+
"bundle_tokens": int,
|
|
117
|
+
"full_history_tokens": int,
|
|
118
|
+
"sessions_count": int,
|
|
119
|
+
"latency_speedup_factor": float,
|
|
120
|
+
},
|
|
121
|
+
...
|
|
122
|
+
}
|
|
123
|
+
"""
|
|
124
|
+
sessions = _read_session_files(gcc_dir / "metrics" / "session")
|
|
125
|
+
by_dev: Dict[str, List[dict]] = {}
|
|
126
|
+
for s in sessions:
|
|
127
|
+
dev_id = s.get("developer_id") or "unknown"
|
|
128
|
+
by_dev.setdefault(dev_id, []).append(s)
|
|
129
|
+
|
|
130
|
+
result: Dict[str, dict] = {}
|
|
131
|
+
for dev_id, dev_sessions in by_dev.items():
|
|
132
|
+
total_used = sum(s.get("tokens_used", 0) for s in dev_sessions)
|
|
133
|
+
total_saved = sum(s.get("tokens_saved", 0) for s in dev_sessions)
|
|
134
|
+
total_bundle = sum(s.get("bundle_tokens", 0) for s in dev_sessions)
|
|
135
|
+
total_full = sum(s.get("full_history_tokens", 0) for s in dev_sessions)
|
|
136
|
+
if total_bundle > 0:
|
|
137
|
+
speedup = total_full / total_bundle
|
|
138
|
+
else:
|
|
139
|
+
speedup = 1.0
|
|
140
|
+
result[dev_id] = {
|
|
141
|
+
"tokens_used": total_used,
|
|
142
|
+
"tokens_saved": total_saved,
|
|
143
|
+
"bundle_tokens": total_bundle,
|
|
144
|
+
"full_history_tokens": total_full,
|
|
145
|
+
"sessions_count": len(dev_sessions),
|
|
146
|
+
"latency_speedup_factor": speedup,
|
|
147
|
+
}
|
|
148
|
+
return result
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def aggregate_all_projects() -> Dict[str, object]:
|
|
152
|
+
"""
|
|
153
|
+
Aggregate token/speedup metrics across all projects in the registry.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
{
|
|
157
|
+
"projects": [
|
|
158
|
+
{
|
|
159
|
+
"name": str,
|
|
160
|
+
"path": str,
|
|
161
|
+
"tokens_used": int,
|
|
162
|
+
"tokens_saved": int,
|
|
163
|
+
"latency_speedup_factor": float,
|
|
164
|
+
"sessions_count": int,
|
|
165
|
+
},
|
|
166
|
+
...
|
|
167
|
+
],
|
|
168
|
+
"aggregate": {
|
|
169
|
+
"tokens_used": int,
|
|
170
|
+
"tokens_saved": int,
|
|
171
|
+
"latency_speedup_factor": float,
|
|
172
|
+
"sessions_count": int,
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
"""
|
|
176
|
+
registry = ProjectRegistry()
|
|
177
|
+
projects = []
|
|
178
|
+
total_used = 0
|
|
179
|
+
total_saved = 0
|
|
180
|
+
total_full = 0
|
|
181
|
+
total_bundle = 0
|
|
182
|
+
total_sessions = 0
|
|
183
|
+
|
|
184
|
+
for entry in registry.list():
|
|
185
|
+
gcc_dir = Path(entry.path) / ".GCC"
|
|
186
|
+
tokens = aggregate_tokens(gcc_dir)
|
|
187
|
+
speedup = aggregate_speedup(gcc_dir)
|
|
188
|
+
projects.append({
|
|
189
|
+
"name": entry.name,
|
|
190
|
+
"path": entry.path,
|
|
191
|
+
"tokens_used": tokens["total_tokens_used"],
|
|
192
|
+
"tokens_saved": tokens["total_tokens_saved"],
|
|
193
|
+
"latency_speedup_factor": speedup["latency_speedup_factor"],
|
|
194
|
+
"sessions_count": tokens["sessions_count"],
|
|
195
|
+
})
|
|
196
|
+
total_used += tokens["total_tokens_used"]
|
|
197
|
+
total_saved += tokens["total_tokens_saved"]
|
|
198
|
+
total_full += speedup["total_full_history_tokens"]
|
|
199
|
+
total_bundle += speedup["total_bundle_tokens"]
|
|
200
|
+
total_sessions += tokens["sessions_count"]
|
|
201
|
+
|
|
202
|
+
if total_bundle > 0:
|
|
203
|
+
aggregate_speedup_factor = total_full / total_bundle
|
|
204
|
+
else:
|
|
205
|
+
aggregate_speedup_factor = 1.0
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
"projects": projects,
|
|
209
|
+
"aggregate": {
|
|
210
|
+
"tokens_used": total_used,
|
|
211
|
+
"tokens_saved": total_saved,
|
|
212
|
+
"latency_speedup_factor": aggregate_speedup_factor,
|
|
213
|
+
"sessions_count": total_sessions,
|
|
214
|
+
},
|
|
215
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Calibration store and onboarding wizard for DevTorch metrics.
|
|
3
|
+
|
|
4
|
+
CalibrationStore: persists team-specific calibration values to .GCC/metrics/calibration.json.
|
|
5
|
+
run_calibrate_wizard: interactive (or non-interactive for tests) onboarding flow.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import subprocess
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Optional, Union
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_CALIBRATION_SUBDIR = "metrics"
|
|
17
|
+
_CALIBRATION_FILE = "calibration.json"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CalibrationStore:
|
|
21
|
+
"""
|
|
22
|
+
Persists calibration values to .GCC/metrics/calibration.json.
|
|
23
|
+
|
|
24
|
+
This is distinct from devtorch_core/metrics/calibration.py's CalibrationStore,
|
|
25
|
+
which lives at .GCC/calibration.json and holds DHS/MCS weights.
|
|
26
|
+
This one lives at .GCC/metrics/calibration.json and holds onboarding baselines.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, gcc_dir: Path) -> None:
|
|
30
|
+
self._gcc_dir = Path(gcc_dir)
|
|
31
|
+
self.calibration_path = self._gcc_dir / _CALIBRATION_SUBDIR / _CALIBRATION_FILE
|
|
32
|
+
|
|
33
|
+
def load(self) -> dict:
|
|
34
|
+
"""Read calibration.json; return {} if missing or corrupt."""
|
|
35
|
+
if not self.calibration_path.exists():
|
|
36
|
+
return {}
|
|
37
|
+
try:
|
|
38
|
+
text = self.calibration_path.read_text(encoding="utf-8")
|
|
39
|
+
data = json.loads(text)
|
|
40
|
+
if not isinstance(data, dict):
|
|
41
|
+
return {}
|
|
42
|
+
return data
|
|
43
|
+
except (json.JSONDecodeError, OSError):
|
|
44
|
+
return {}
|
|
45
|
+
|
|
46
|
+
def save(self, data: dict) -> None:
|
|
47
|
+
"""Write data to .GCC/metrics/calibration.json, creating dirs if needed."""
|
|
48
|
+
self.calibration_path.parent.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
self.calibration_path.write_text(
|
|
50
|
+
json.dumps(data, indent=2, sort_keys=True),
|
|
51
|
+
encoding="utf-8",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def set_value(self, key: str, value: Union[float, str]) -> None:
|
|
55
|
+
"""Set a single calibration key and persist."""
|
|
56
|
+
data = self.load()
|
|
57
|
+
data[key] = value
|
|
58
|
+
self.save(data)
|
|
59
|
+
|
|
60
|
+
def get_value(self, key: str, default: Any = None) -> Any:
|
|
61
|
+
"""Get a single calibration key, returning default if not set."""
|
|
62
|
+
data = self.load()
|
|
63
|
+
return data.get(key, default)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _estimate_cold_start_min(repo_root: Path) -> float:
|
|
67
|
+
"""
|
|
68
|
+
Estimate cold_start_min from git history.
|
|
69
|
+
|
|
70
|
+
Counts commits in the last 30 days and computes the average inter-commit
|
|
71
|
+
interval as a rough proxy for context switch / cold-start time.
|
|
72
|
+
Falls back to 15.0 minutes if git is unavailable or the repo has no history.
|
|
73
|
+
"""
|
|
74
|
+
default = 15.0
|
|
75
|
+
try:
|
|
76
|
+
result = subprocess.run(
|
|
77
|
+
["git", "-C", str(repo_root), "log", "--format=%ct", "--since=30.days.ago"],
|
|
78
|
+
capture_output=True,
|
|
79
|
+
text=True,
|
|
80
|
+
timeout=10,
|
|
81
|
+
)
|
|
82
|
+
if result.returncode != 0:
|
|
83
|
+
return default
|
|
84
|
+
timestamps = [int(t.strip()) for t in result.stdout.splitlines() if t.strip()]
|
|
85
|
+
if len(timestamps) < 2:
|
|
86
|
+
return default
|
|
87
|
+
# timestamps are newest-first; sort ascending
|
|
88
|
+
timestamps.sort()
|
|
89
|
+
intervals_seconds = [
|
|
90
|
+
timestamps[i + 1] - timestamps[i]
|
|
91
|
+
for i in range(len(timestamps) - 1)
|
|
92
|
+
]
|
|
93
|
+
avg_seconds = sum(intervals_seconds) / len(intervals_seconds)
|
|
94
|
+
avg_minutes = avg_seconds / 60.0
|
|
95
|
+
# Clamp to a sane range [1, 120]
|
|
96
|
+
return max(1.0, min(120.0, avg_minutes))
|
|
97
|
+
except Exception:
|
|
98
|
+
return default
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def run_calibrate_wizard(
|
|
102
|
+
gcc_dir: Path,
|
|
103
|
+
values: Optional[dict] = None,
|
|
104
|
+
) -> dict:
|
|
105
|
+
"""
|
|
106
|
+
Run the calibration onboarding wizard.
|
|
107
|
+
|
|
108
|
+
If *values* is provided, the wizard runs non-interactively using those values
|
|
109
|
+
(useful for tests and automation). Missing keys fall back to defaults.
|
|
110
|
+
|
|
111
|
+
If *values* is None, the wizard prompts the user interactively.
|
|
112
|
+
|
|
113
|
+
Returns the calibration dict that was saved.
|
|
114
|
+
|
|
115
|
+
Fields:
|
|
116
|
+
cold_start_min — estimated from git history (or prompted)
|
|
117
|
+
avg_rework_hours — default 2.0
|
|
118
|
+
token_cost_usd_per_million — required, no default (model pricing varies 5×)
|
|
119
|
+
developer_hourly_rate — required, no default
|
|
120
|
+
auditor_hourly_rate — default 39.0
|
|
121
|
+
sprint_length_days — default 14
|
|
122
|
+
complex_problem_delivery_time_baseline_min — default 240.0 (4 hours). Team-calibrated input.
|
|
123
|
+
"""
|
|
124
|
+
repo_root = gcc_dir.parent if gcc_dir.name == ".GCC" else gcc_dir
|
|
125
|
+
|
|
126
|
+
git_estimate = _estimate_cold_start_min(repo_root)
|
|
127
|
+
|
|
128
|
+
if values is not None:
|
|
129
|
+
# Non-interactive mode
|
|
130
|
+
token_cost_raw = values.get("token_cost_usd_per_million")
|
|
131
|
+
calibration = {
|
|
132
|
+
"cold_start_min": float(values.get("cold_start_min", git_estimate)),
|
|
133
|
+
"avg_rework_hours": float(values.get("avg_rework_hours", 2.0)),
|
|
134
|
+
"developer_hourly_rate": float(values["developer_hourly_rate"]),
|
|
135
|
+
"auditor_hourly_rate": float(values.get("auditor_hourly_rate", 39.0)),
|
|
136
|
+
"sprint_length_days": int(values.get("sprint_length_days", 14)),
|
|
137
|
+
# 240 min = 4 hours. This is a team-calibrated baseline, not a universal
|
|
138
|
+
# constant; teams should adjust it via the wizard or `calibrate --set`.
|
|
139
|
+
"complex_problem_delivery_time_baseline_min": float(values.get("complex_problem_delivery_time_baseline_min", 240.0)),
|
|
140
|
+
}
|
|
141
|
+
if token_cost_raw is not None:
|
|
142
|
+
calibration["token_cost_usd_per_million"] = float(token_cost_raw)
|
|
143
|
+
else:
|
|
144
|
+
# Interactive mode
|
|
145
|
+
print("DevTorch Metrics Calibration Wizard")
|
|
146
|
+
print("=====================================")
|
|
147
|
+
print("Press Enter to accept defaults shown in [brackets].\n")
|
|
148
|
+
|
|
149
|
+
def _prompt_float(prompt: str, default: Optional[float]) -> float:
|
|
150
|
+
default_str = f" [{default}]" if default is not None else ""
|
|
151
|
+
while True:
|
|
152
|
+
raw = input(f"{prompt}{default_str}: ").strip()
|
|
153
|
+
if not raw and default is not None:
|
|
154
|
+
return default
|
|
155
|
+
try:
|
|
156
|
+
return float(raw)
|
|
157
|
+
except ValueError:
|
|
158
|
+
print(" Please enter a numeric value.")
|
|
159
|
+
|
|
160
|
+
def _prompt_int(prompt: str, default: int) -> int:
|
|
161
|
+
while True:
|
|
162
|
+
raw = input(f"{prompt} [{default}]: ").strip()
|
|
163
|
+
if not raw:
|
|
164
|
+
return default
|
|
165
|
+
try:
|
|
166
|
+
return int(raw)
|
|
167
|
+
except ValueError:
|
|
168
|
+
print(" Please enter an integer value.")
|
|
169
|
+
|
|
170
|
+
cold_start_min = _prompt_float(
|
|
171
|
+
f"Cold-start time (minutes, estimated from git: {git_estimate:.1f})",
|
|
172
|
+
git_estimate,
|
|
173
|
+
)
|
|
174
|
+
avg_rework_hours = _prompt_float("Average rework hours per collision", 2.0)
|
|
175
|
+
complex_problem_delivery_time_baseline_min = _prompt_float(
|
|
176
|
+
"Complex problem delivery-time baseline (minutes, team-calibrated)", 240.0
|
|
177
|
+
)
|
|
178
|
+
token_cost_usd_per_million = _prompt_float(
|
|
179
|
+
"Token cost (USD per million tokens, required)", None
|
|
180
|
+
)
|
|
181
|
+
developer_hourly_rate = _prompt_float("Developer hourly rate (USD, required)", None)
|
|
182
|
+
auditor_hourly_rate = _prompt_float("Auditor hourly rate (USD)", 39.0)
|
|
183
|
+
sprint_length_days = _prompt_int("Sprint length (days)", 14)
|
|
184
|
+
|
|
185
|
+
calibration = {
|
|
186
|
+
"cold_start_min": cold_start_min,
|
|
187
|
+
"avg_rework_hours": avg_rework_hours,
|
|
188
|
+
"complex_problem_delivery_time_baseline_min": complex_problem_delivery_time_baseline_min,
|
|
189
|
+
"developer_hourly_rate": developer_hourly_rate,
|
|
190
|
+
"auditor_hourly_rate": auditor_hourly_rate,
|
|
191
|
+
"sprint_length_days": sprint_length_days,
|
|
192
|
+
}
|
|
193
|
+
if token_cost_usd_per_million is not None:
|
|
194
|
+
calibration["token_cost_usd_per_million"] = token_cost_usd_per_million
|
|
195
|
+
|
|
196
|
+
store = CalibrationStore(gcc_dir)
|
|
197
|
+
store.save(calibration)
|
|
198
|
+
return calibration
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Metrics calibration store.
|
|
3
|
+
|
|
4
|
+
Calibration values are team-specific. The DEFAULTS below are reasonable
|
|
5
|
+
starting points but MUST be adjusted with real team data for meaningful
|
|
6
|
+
numbers. Do not treat defaults as validated benchmarks.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from dataclasses import asdict, dataclass, fields
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Optional
|
|
15
|
+
|
|
16
|
+
CALIBRATION_FILE_NAME = "calibration.json"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class CalibrationDefaults:
|
|
21
|
+
"""
|
|
22
|
+
Default calibration values for DevTorch metrics.
|
|
23
|
+
|
|
24
|
+
Each field is annotated with its source citation or "design choice,
|
|
25
|
+
calibrate with team data" for values with no external benchmark.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
# === Context switching cost ===
|
|
29
|
+
# Source: Parnin & Rugaber (2011) Software Quality Journal
|
|
30
|
+
# DOI: 10.1007/s11219-010-9104-9
|
|
31
|
+
# Measured: 15 min for programming tasks (not general work)
|
|
32
|
+
context_switch_minutes: float = 15.0
|
|
33
|
+
|
|
34
|
+
# === Developer hourly rate ===
|
|
35
|
+
# No universal default — teams must provide this.
|
|
36
|
+
# BLS SOC 15-1252 (2023): $55-65/hr median for software developers
|
|
37
|
+
# Set to 0 to force explicit calibration.
|
|
38
|
+
developer_hourly_rate: float = 0.0
|
|
39
|
+
|
|
40
|
+
# === Rework time per collision ===
|
|
41
|
+
# No peer-reviewed benchmark. Design choice. Calibrate with team data.
|
|
42
|
+
rework_minutes_per_collision: float = 30.0
|
|
43
|
+
|
|
44
|
+
# === MCS weights ===
|
|
45
|
+
# Design choices. No external benchmark. Calibrate with team data.
|
|
46
|
+
mcs_weight_frequency: float = 0.4
|
|
47
|
+
mcs_weight_severity: float = 0.4
|
|
48
|
+
mcs_weight_resolution_time: float = 0.2
|
|
49
|
+
|
|
50
|
+
# === DHS weights ===
|
|
51
|
+
# Design choices. No external benchmark. Calibrate with team data.
|
|
52
|
+
dhs_weight_commit_health: float = 0.3
|
|
53
|
+
dhs_weight_sensitivity_health: float = 0.25
|
|
54
|
+
dhs_weight_capability_health: float = 0.25
|
|
55
|
+
dhs_weight_variance_health: float = 0.2
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
_DEFAULTS = CalibrationDefaults()
|
|
59
|
+
_DEFAULTS_DICT: dict[str, Any] = {f.name: getattr(_DEFAULTS, f.name) for f in fields(_DEFAULTS)}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class CalibrationStore:
|
|
63
|
+
"""
|
|
64
|
+
Calibration value store backed by `.GCC/calibration.json`.
|
|
65
|
+
|
|
66
|
+
Overrides in the JSON file take precedence over CalibrationDefaults.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(self, gcc_root: str) -> None:
|
|
70
|
+
self._gcc_root = Path(gcc_root)
|
|
71
|
+
self._path = self._gcc_root / CALIBRATION_FILE_NAME
|
|
72
|
+
self._overrides: dict[str, Any] = self.load()
|
|
73
|
+
|
|
74
|
+
def load(self) -> dict:
|
|
75
|
+
"""Read calibration JSON; return empty dict on missing or corrupt file."""
|
|
76
|
+
if not self._path.exists():
|
|
77
|
+
return {}
|
|
78
|
+
try:
|
|
79
|
+
text = self._path.read_text(encoding="utf-8")
|
|
80
|
+
data = json.loads(text)
|
|
81
|
+
if not isinstance(data, dict):
|
|
82
|
+
return {}
|
|
83
|
+
return data
|
|
84
|
+
except (json.JSONDecodeError, OSError):
|
|
85
|
+
return {}
|
|
86
|
+
|
|
87
|
+
def save(self, values: dict) -> None:
|
|
88
|
+
"""Write the given values dict to .GCC/calibration.json."""
|
|
89
|
+
self._gcc_root.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
self._path.write_text(
|
|
91
|
+
json.dumps(values, indent=2, sort_keys=True),
|
|
92
|
+
encoding="utf-8",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
96
|
+
"""Return the calibration value for key (overrides > defaults > default arg)."""
|
|
97
|
+
if key in self._overrides:
|
|
98
|
+
return self._overrides[key]
|
|
99
|
+
if key in _DEFAULTS_DICT:
|
|
100
|
+
return _DEFAULTS_DICT[key]
|
|
101
|
+
return default
|
|
102
|
+
|
|
103
|
+
def get_all(self) -> dict:
|
|
104
|
+
"""Return the full merged dict: defaults overridden by any stored values."""
|
|
105
|
+
merged = dict(_DEFAULTS_DICT)
|
|
106
|
+
merged.update(self._overrides)
|
|
107
|
+
return merged
|
|
108
|
+
|
|
109
|
+
def set(self, key: str, value: Any) -> None:
|
|
110
|
+
"""Set an override value and persist it to disk."""
|
|
111
|
+
self._overrides[key] = value
|
|
112
|
+
self.save(self._overrides)
|
|
113
|
+
|
|
114
|
+
def reset(self, key: str) -> None:
|
|
115
|
+
"""Remove an override, falling back to the default value."""
|
|
116
|
+
if key in self._overrides:
|
|
117
|
+
del self._overrides[key]
|
|
118
|
+
self.save(self._overrides)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def load_calibration(gcc_root: Optional[str] = None) -> CalibrationStore:
|
|
122
|
+
"""Convenience factory. If gcc_root is None, uses '.GCC' in the current directory."""
|
|
123
|
+
if gcc_root is None:
|
|
124
|
+
gcc_root = ".GCC"
|
|
125
|
+
return CalibrationStore(gcc_root)
|