devtorch-core 3.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. devtorch_core/__init__.py +158 -0
  2. devtorch_core/aggphi_textual.py +275 -0
  3. devtorch_core/alerts/__init__.py +23 -0
  4. devtorch_core/alerts/base.py +46 -0
  5. devtorch_core/alerts/config.py +60 -0
  6. devtorch_core/alerts/dispatcher.py +110 -0
  7. devtorch_core/alerts/jira.py +96 -0
  8. devtorch_core/alerts/linear.py +72 -0
  9. devtorch_core/alerts/pagerduty.py +66 -0
  10. devtorch_core/alerts/slack.py +81 -0
  11. devtorch_core/alerts/teams.py +70 -0
  12. devtorch_core/audit/__init__.py +43 -0
  13. devtorch_core/audit/exporter.py +297 -0
  14. devtorch_core/audit/privacy.py +101 -0
  15. devtorch_core/audit/scrubber.py +149 -0
  16. devtorch_core/audit/service.py +67 -0
  17. devtorch_core/audit/signing.py +127 -0
  18. devtorch_core/broadcast/__init__.py +4 -0
  19. devtorch_core/broadcast/broadcaster.py +100 -0
  20. devtorch_core/broadcast/watcher.py +71 -0
  21. devtorch_core/capability.py +639 -0
  22. devtorch_core/cloud/__init__.py +1 -0
  23. devtorch_core/cloud/client_config.py +472 -0
  24. devtorch_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
  25. devtorch_core/cloud/client_configs/.claude-stdio.json +13 -0
  26. devtorch_core/cloud/client_configs/.cursor-mcp.json +13 -0
  27. devtorch_core/cloud/client_configs/.opencode-bridge.json +13 -0
  28. devtorch_core/cloud/client_configs/.opencode.json +15 -0
  29. devtorch_core/cloud/client_configs/.vscode-mcp.json +13 -0
  30. devtorch_core/cloud/devtorch-mcp-bridge.js +357 -0
  31. devtorch_core/cloud/mcp_client.py +229 -0
  32. devtorch_core/cloud/setup.py +144 -0
  33. devtorch_core/cloud/sync.py +143 -0
  34. devtorch_core/cloud/sync_bundle.py +603 -0
  35. devtorch_core/cloud/sync_conflicts.py +159 -0
  36. devtorch_core/cloud/sync_state.py +159 -0
  37. devtorch_core/cloud/team_sync.py +283 -0
  38. devtorch_core/codex/__init__.py +9 -0
  39. devtorch_core/codex/__main__.py +97 -0
  40. devtorch_core/codex/capture.py +208 -0
  41. devtorch_core/codex/proxy.py +412 -0
  42. devtorch_core/concept_catalog.py +209 -0
  43. devtorch_core/consolidation/__init__.py +3 -0
  44. devtorch_core/consolidation/synthesizer.py +87 -0
  45. devtorch_core/consolidation/workflow.py +175 -0
  46. devtorch_core/daemon/__init__.py +27 -0
  47. devtorch_core/daemon/supervisor.py +293 -0
  48. devtorch_core/daemon/watcher.py +244 -0
  49. devtorch_core/dashboard_api.py +2012 -0
  50. devtorch_core/deltaf.py +97 -0
  51. devtorch_core/disclosure.py +50 -0
  52. devtorch_core/divergence/__init__.py +3 -0
  53. devtorch_core/divergence/detector.py +166 -0
  54. devtorch_core/gateway/__init__.py +32 -0
  55. devtorch_core/gateway/key_manager.py +124 -0
  56. devtorch_core/gateway/metrics_webhook.py +252 -0
  57. devtorch_core/gateway/policy.py +262 -0
  58. devtorch_core/gateway/server.py +727 -0
  59. devtorch_core/gateway/sso.py +233 -0
  60. devtorch_core/gcc.py +1246 -0
  61. devtorch_core/github/__init__.py +35 -0
  62. devtorch_core/github/app.py +240 -0
  63. devtorch_core/github/comment_builder.py +113 -0
  64. devtorch_core/github/pat.py +76 -0
  65. devtorch_core/github/pr_parser.py +82 -0
  66. devtorch_core/github/pr_reporter.py +555 -0
  67. devtorch_core/gitlab/__init__.py +177 -0
  68. devtorch_core/hitl/__init__.py +4 -0
  69. devtorch_core/hitl/channels.py +129 -0
  70. devtorch_core/hitl/orchestrator.py +95 -0
  71. devtorch_core/hooks/__init__.py +17 -0
  72. devtorch_core/hooks/claude_code.py +228 -0
  73. devtorch_core/hooks/git_capture.py +341 -0
  74. devtorch_core/hooks/git_commit.py +182 -0
  75. devtorch_core/hooks/installer.py +733 -0
  76. devtorch_core/hooks/pre_commit.py +157 -0
  77. devtorch_core/hooks/runner.py +344 -0
  78. devtorch_core/identity/__init__.py +4 -0
  79. devtorch_core/identity/agent.py +86 -0
  80. devtorch_core/identity/providers.py +85 -0
  81. devtorch_core/invariants.py +182 -0
  82. devtorch_core/mcp/__init__.py +10 -0
  83. devtorch_core/mcp/auth.py +177 -0
  84. devtorch_core/mcp/server.py +1049 -0
  85. devtorch_core/metrics/__init__.py +35 -0
  86. devtorch_core/metrics/aggregate.py +215 -0
  87. devtorch_core/metrics/calibrate.py +198 -0
  88. devtorch_core/metrics/calibration.py +125 -0
  89. devtorch_core/metrics/credibility.py +288 -0
  90. devtorch_core/metrics/delivery_time.py +70 -0
  91. devtorch_core/metrics/dhs.py +126 -0
  92. devtorch_core/metrics/mcs.py +96 -0
  93. devtorch_core/metrics/roi.py +88 -0
  94. devtorch_core/metrics/session_writer.py +81 -0
  95. devtorch_core/metrics/shadow_ai.py +117 -0
  96. devtorch_core/metrics/sprint_writer.py +243 -0
  97. devtorch_core/observability/__init__.py +78 -0
  98. devtorch_core/observability/datadog.py +157 -0
  99. devtorch_core/observability/formatter.py +119 -0
  100. devtorch_core/observability/report.py +264 -0
  101. devtorch_core/observability/servicenow.py +147 -0
  102. devtorch_core/observability/splunk.py +218 -0
  103. devtorch_core/observability/webhook.py +227 -0
  104. devtorch_core/parser/__init__.py +30 -0
  105. devtorch_core/parser/blocks.py +216 -0
  106. devtorch_core/parser/inference.py +159 -0
  107. devtorch_core/parser/thinking.py +112 -0
  108. devtorch_core/projects.py +169 -0
  109. devtorch_core/prompt_artifact.py +76 -0
  110. devtorch_core/proxy/__init__.py +9 -0
  111. devtorch_core/proxy/routes/__init__.py +1 -0
  112. devtorch_core/proxy/routes/anthropic.py +264 -0
  113. devtorch_core/proxy/routes/azure_openai.py +336 -0
  114. devtorch_core/proxy/routes/gemini.py +331 -0
  115. devtorch_core/proxy/routes/groq.py +284 -0
  116. devtorch_core/proxy/routes/ollama.py +279 -0
  117. devtorch_core/proxy/routes/openai.py +287 -0
  118. devtorch_core/proxy/server.py +356 -0
  119. devtorch_core/query/__init__.py +15 -0
  120. devtorch_core/query/grep.py +181 -0
  121. devtorch_core/query/hybrid.py +86 -0
  122. devtorch_core/query/semantic.py +157 -0
  123. devtorch_core/rdp.py +105 -0
  124. devtorch_core/reasoning/__init__.py +4 -0
  125. devtorch_core/reasoning/entry.py +31 -0
  126. devtorch_core/reasoning/store.py +122 -0
  127. devtorch_core/reasoning_plus/__init__.py +70 -0
  128. devtorch_core/reasoning_plus/augmenter.py +326 -0
  129. devtorch_core/reasoning_plus/capture.py +51 -0
  130. devtorch_core/reasoning_plus/config.py +256 -0
  131. devtorch_core/reasoning_plus/context.py +262 -0
  132. devtorch_core/reasoning_plus/learning/__init__.py +72 -0
  133. devtorch_core/reasoning_plus/learning/analytics.py +141 -0
  134. devtorch_core/reasoning_plus/learning/api.py +313 -0
  135. devtorch_core/reasoning_plus/learning/chain.py +285 -0
  136. devtorch_core/reasoning_plus/learning/composer.py +74 -0
  137. devtorch_core/reasoning_plus/learning/cross_project.py +234 -0
  138. devtorch_core/reasoning_plus/learning/embeddings.py +209 -0
  139. devtorch_core/reasoning_plus/learning/extractor.py +207 -0
  140. devtorch_core/reasoning_plus/learning/models.py +116 -0
  141. devtorch_core/reasoning_plus/learning/provenance.py +126 -0
  142. devtorch_core/reasoning_plus/learning/recorder.py +81 -0
  143. devtorch_core/reasoning_plus/learning/relevance.py +122 -0
  144. devtorch_core/reasoning_plus/learning/state.py +86 -0
  145. devtorch_core/reasoning_plus/learning/store.py +160 -0
  146. devtorch_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
  147. devtorch_core/reasoning_plus/prompt.py +90 -0
  148. devtorch_core/rep.py +134 -0
  149. devtorch_core/rep_network/__init__.py +25 -0
  150. devtorch_core/rep_network/merge.py +70 -0
  151. devtorch_core/rep_network/node.py +137 -0
  152. devtorch_core/rep_network/server.py +140 -0
  153. devtorch_core/rep_network/sync.py +207 -0
  154. devtorch_core/sensitivity.py +182 -0
  155. devtorch_core/serve.py +258 -0
  156. devtorch_core/session/__init__.py +39 -0
  157. devtorch_core/session/disagreement.py +188 -0
  158. devtorch_core/session/models.py +114 -0
  159. devtorch_core/session/orchestrator.py +182 -0
  160. devtorch_core/session/planner.py +169 -0
  161. devtorch_core/session/simulator.py +132 -0
  162. devtorch_core/signing.py +290 -0
  163. devtorch_core/sis.py +197 -0
  164. devtorch_core/storage.py +308 -0
  165. devtorch_core/templates/__init__.py +6 -0
  166. devtorch_core/templates/engine.py +122 -0
  167. devtorch_core/templates/go.py +18 -0
  168. devtorch_core/templates/infra.py +19 -0
  169. devtorch_core/templates/library/__init__.py +18 -0
  170. devtorch_core/templates/library/api_design.md +27 -0
  171. devtorch_core/templates/library/bug_fix.md +27 -0
  172. devtorch_core/templates/library/decision_record.md +27 -0
  173. devtorch_core/templates/library/engine.py +228 -0
  174. devtorch_core/templates/library/security_review.md +30 -0
  175. devtorch_core/templates/python.py +19 -0
  176. devtorch_core/templates/react.py +18 -0
  177. devtorch_core/templates/typescript.py +18 -0
  178. devtorch_core/theta.py +221 -0
  179. devtorch_core/theta_synthesis.py +268 -0
  180. devtorch_core/topics.py +320 -0
  181. devtorch_core/variance.py +219 -0
  182. devtorch_core/wrapper/__init__.py +52 -0
  183. devtorch_core/wrapper/anthropic.py +487 -0
  184. devtorch_core/wrapper/base.py +562 -0
  185. devtorch_core/wrapper/bedrock.py +342 -0
  186. devtorch_core/wrapper/gemini.py +422 -0
  187. devtorch_core/wrapper/ollama.py +527 -0
  188. devtorch_core/wrapper/openai.py +461 -0
  189. devtorch_core-3.0.1.dist-info/METADATA +867 -0
  190. devtorch_core-3.0.1.dist-info/RECORD +193 -0
  191. devtorch_core-3.0.1.dist-info/WHEEL +5 -0
  192. devtorch_core-3.0.1.dist-info/entry_points.txt +2 -0
  193. devtorch_core-3.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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)