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,341 @@
1
+ """
2
+ S26 git capture hooks — always-on capture for commit, merge, checkout, rebase,
3
+ and conflict events.
4
+
5
+ Entry point: devtorch hooks git-capture <event> [args...]
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import re
12
+ import subprocess
13
+ from pathlib import Path
14
+ from typing import Optional
15
+
16
+
17
+ # ---------------------------------------------------------------------------
18
+ # .GCC/ repo discovery
19
+ # ---------------------------------------------------------------------------
20
+
21
+
22
+ def _find_repo() -> Optional[object]:
23
+ """Walk upward from cwd to find an initialised .GCC/ repo."""
24
+ from devtorch_core import GCCRepository
25
+
26
+ for parent in [Path.cwd()] + list(Path.cwd().parents):
27
+ r = GCCRepository.at(parent)
28
+ if r.is_initialized():
29
+ return r
30
+ return None
31
+
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Event appender
35
+ # ---------------------------------------------------------------------------
36
+
37
+
38
+ def _append_event(repo, event_type: str, payload: dict) -> None:
39
+ """Append a raw event to the repo's event log."""
40
+ try:
41
+ repo._append_event(event_type, payload)
42
+ except Exception:
43
+ # _append_event may not be public on all versions; fall back to
44
+ # writing directly to events.log.jsonl
45
+ try:
46
+ import fcntl
47
+ import hashlib
48
+ import time
49
+
50
+ log_path = Path(repo.gcc_dir) / "events.log.jsonl"
51
+ log_path.parent.mkdir(parents=True, exist_ok=True)
52
+ with log_path.open("a", encoding="utf-8") as f:
53
+ fcntl.flock(f, fcntl.LOCK_EX)
54
+ try:
55
+ prev_hash = "0" * 64
56
+ if log_path.exists():
57
+ lines = log_path.read_text(encoding="utf-8").strip().splitlines()
58
+ if lines:
59
+ last = json.loads(lines[-1])
60
+ prev_hash = last.get("hash", prev_hash)
61
+
62
+ entry = {
63
+ "type": event_type,
64
+ "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
65
+ **payload,
66
+ }
67
+ entry_str = json.dumps(entry, sort_keys=True)
68
+ entry["hash"] = hashlib.sha256(
69
+ (prev_hash + entry_str).encode()
70
+ ).hexdigest()
71
+
72
+ f.write(json.dumps(entry) + "\n")
73
+ f.flush()
74
+ finally:
75
+ fcntl.flock(f, fcntl.LOCK_UN)
76
+ except Exception:
77
+ pass
78
+
79
+
80
+ # ---------------------------------------------------------------------------
81
+ # Git metadata helpers
82
+ # ---------------------------------------------------------------------------
83
+
84
+
85
+ def _run_git(repo_root: Path, *args: str, timeout: float = 10.0) -> str:
86
+ """Run a git command and return stripped stdout, or empty string on failure."""
87
+ try:
88
+ result = subprocess.run(
89
+ ["git", *args],
90
+ cwd=repo_root,
91
+ capture_output=True,
92
+ text=True,
93
+ check=True,
94
+ timeout=timeout,
95
+ )
96
+ return result.stdout.strip()
97
+ except Exception:
98
+ return ""
99
+
100
+
101
+ def _current_branch(repo: object) -> str:
102
+ """Best-effort current branch name from git or GCC repo."""
103
+ try:
104
+ branch = _run_git(repo.root, "rev-parse", "--abbrev-ref", "HEAD")
105
+ if branch:
106
+ return branch
107
+ except Exception:
108
+ pass
109
+ return getattr(repo, "_current_branch", lambda: "main")()
110
+
111
+
112
+ def _head_sha(repo: object, ref: str = "HEAD") -> str:
113
+ """Best-effort commit SHA for the given ref."""
114
+ return _run_git(repo.root, "rev-parse", ref)
115
+
116
+
117
+ # ---------------------------------------------------------------------------
118
+ # Merge conflict detection
119
+ # ---------------------------------------------------------------------------
120
+
121
+
122
+ _CONFLICT_MARKER_RE = re.compile(r"^(<{7}|={7}|>{7})", re.MULTILINE)
123
+
124
+
125
+ def _has_conflict_markers(repo_root: Path) -> bool:
126
+ """Scan tracked files in the working tree for conflict markers."""
127
+ try:
128
+ # Ask git for files with changes (staged or unstaged)
129
+ result = subprocess.run(
130
+ ["git", "diff", "--name-only"],
131
+ cwd=repo_root,
132
+ capture_output=True,
133
+ text=True,
134
+ timeout=10.0,
135
+ )
136
+ files = result.stdout.strip().splitlines()
137
+ if not files:
138
+ # Also check staged diffs
139
+ result = subprocess.run(
140
+ ["git", "diff", "--cached", "--name-only"],
141
+ cwd=repo_root,
142
+ capture_output=True,
143
+ text=True,
144
+ timeout=10.0,
145
+ )
146
+ files = result.stdout.strip().splitlines()
147
+
148
+ for line in files:
149
+ fpath = repo_root / line
150
+ if fpath.is_file():
151
+ try:
152
+ text = fpath.read_text(encoding="utf-8", errors="ignore")
153
+ if _CONFLICT_MARKER_RE.search(text):
154
+ return True
155
+ except Exception:
156
+ pass
157
+ except Exception:
158
+ pass
159
+ return False
160
+
161
+
162
+ def _merge_state_files(repo_root: Path) -> bool:
163
+ """Check the classic .git merge state files."""
164
+ git_dir = repo_root / ".git"
165
+ try:
166
+ if (git_dir / "MERGE_HEAD").exists():
167
+ return True
168
+ if (git_dir / "MERGE_MSG").exists():
169
+ return True
170
+ except Exception:
171
+ pass
172
+ return False
173
+
174
+
175
+ def detect_merge_conflict(repo: object) -> bool:
176
+ """Detect whether the repository is currently in a merge conflict state."""
177
+ try:
178
+ repo_root = Path(repo.root)
179
+ if _merge_state_files(repo_root):
180
+ return True
181
+ if _has_conflict_markers(repo_root):
182
+ return True
183
+ except Exception:
184
+ pass
185
+ return False
186
+
187
+
188
+ def _emit_conflict_if_detected(repo: object, trigger: str) -> None:
189
+ """Helper: emit a git_conflict event if the repo is in conflict state."""
190
+ if detect_merge_conflict(repo):
191
+ _append_event(repo, "git_conflict", {
192
+ "branch": _current_branch(repo),
193
+ "trigger": trigger,
194
+ "kind": "merge_conflict",
195
+ })
196
+
197
+
198
+ # ---------------------------------------------------------------------------
199
+ # Git hook handlers
200
+ # ---------------------------------------------------------------------------
201
+
202
+
203
+ def handle_git_commit(event_name: str = "post-commit", args: Optional[list[str]] = None) -> None:
204
+ """post-commit hook entry point."""
205
+ repo = _find_repo()
206
+ if repo is None:
207
+ return
208
+ _append_event(repo, "git_commit", {
209
+ "branch": _current_branch(repo),
210
+ "commit_sha": _head_sha(repo, "HEAD"),
211
+ "hook": event_name,
212
+ "args": args or [],
213
+ })
214
+ _emit_conflict_if_detected(repo, event_name)
215
+
216
+
217
+ def handle_git_merge(event_name: str = "post-merge", args: Optional[list[str]] = None) -> None:
218
+ """post-merge hook entry point."""
219
+ repo = _find_repo()
220
+ if repo is None:
221
+ return
222
+ branch = _current_branch(repo)
223
+ payload: dict = {
224
+ "branch": branch,
225
+ "hook": event_name,
226
+ "args": args or [],
227
+ }
228
+ if args:
229
+ payload["squash_flag"] = args[0]
230
+ _append_event(repo, "git_merge", payload)
231
+ _emit_conflict_if_detected(repo, event_name)
232
+
233
+
234
+ def handle_git_checkout(event_name: str = "post-checkout", args: Optional[list[str]] = None) -> None:
235
+ """post-checkout hook entry point."""
236
+ repo = _find_repo()
237
+ if repo is None:
238
+ return
239
+ branch = _current_branch(repo)
240
+ arg_list = args or []
241
+ payload: dict = {
242
+ "branch": branch,
243
+ "hook": event_name,
244
+ "args": arg_list,
245
+ }
246
+ if len(arg_list) > 0:
247
+ payload["prev_head"] = arg_list[0]
248
+ if len(arg_list) > 1:
249
+ payload["new_head"] = arg_list[1]
250
+ if len(arg_list) > 2:
251
+ payload["flag"] = arg_list[2]
252
+ _append_event(repo, "git_checkout", payload)
253
+ _emit_conflict_if_detected(repo, event_name)
254
+
255
+
256
+ def handle_git_rebase(event_name: str = "post-rewrite", args: Optional[list[str]] = None) -> None:
257
+ """post-rewrite hook entry point (rebase / amend)."""
258
+ repo = _find_repo()
259
+ if repo is None:
260
+ return
261
+ branch = _current_branch(repo)
262
+ command = (args or [""])[0] if args else ""
263
+ if command == "rebase":
264
+ _append_event(repo, "git_rebase", {
265
+ "branch": branch,
266
+ "hook": event_name,
267
+ "command": command,
268
+ "args": args or [],
269
+ })
270
+ _emit_conflict_if_detected(repo, event_name)
271
+
272
+
273
+ def handle_git_conflict() -> None:
274
+ """Manual / helper invocation: emit a git_conflict event if in conflict."""
275
+ repo = _find_repo()
276
+ if repo is None:
277
+ return
278
+ _emit_conflict_if_detected(repo, "manual")
279
+
280
+
281
+ # ---------------------------------------------------------------------------
282
+ # Dispatch
283
+ # ---------------------------------------------------------------------------
284
+
285
+ _HANDLERS = {
286
+ "post-commit": handle_git_commit,
287
+ "post-merge": handle_git_merge,
288
+ "post-checkout": handle_git_checkout,
289
+ "post-rewrite": handle_git_rebase,
290
+ "conflict": handle_git_conflict,
291
+ }
292
+
293
+
294
+ def handle_git_capture(event_name: str, args: Optional[list[str]] = None) -> None:
295
+ """
296
+ Entry point called by `devtorch hooks git-capture <event> [args...]`.
297
+ Dispatches to the correct handler. Unknown events are ignored silently.
298
+ """
299
+ handler = _HANDLERS.get(event_name)
300
+ if handler:
301
+ try:
302
+ handler(event_name, args)
303
+ except Exception:
304
+ pass # never interfere with git
305
+
306
+
307
+ # ---------------------------------------------------------------------------
308
+ # Hook installer
309
+ # ---------------------------------------------------------------------------
310
+
311
+ _GIT_CAPTURE_HOOKS: dict[str, str] = {
312
+ "post-commit": "devtorch hooks git-capture post-commit \"$@\"",
313
+ "post-merge": "devtorch hooks git-capture post-merge \"$@\"",
314
+ "post-checkout": "devtorch hooks git-capture post-checkout \"$@\"",
315
+ "post-rewrite": "devtorch hooks git-capture post-rewrite \"$@\"",
316
+ }
317
+
318
+
319
+ def install_git_capture_hooks(repo_root: Path | str) -> tuple[bool, dict[str, str]]:
320
+ """
321
+ Install the S26 git capture hooks in .git/hooks/.
322
+
323
+ Returns (all_ok, {hook_name: path or error_message}).
324
+ """
325
+ hooks_dir = Path(repo_root) / ".git" / "hooks"
326
+ if not hooks_dir.is_dir():
327
+ return False, {}
328
+
329
+ written: dict[str, str] = {}
330
+ all_ok = True
331
+ for hook_name, body in _GIT_CAPTURE_HOOKS.items():
332
+ hook_file = hooks_dir / hook_name
333
+ try:
334
+ hook_script = f"#!/bin/sh\n{body}\n"
335
+ hook_file.write_text(hook_script, encoding="utf-8")
336
+ hook_file.chmod(0o755)
337
+ written[hook_name] = str(hook_file)
338
+ except Exception as exc:
339
+ all_ok = False
340
+ written[hook_name] = str(exc)
341
+ return all_ok, written
@@ -0,0 +1,182 @@
1
+ """
2
+ S7 git commit-msg hook — human developer capture.
3
+
4
+ Intercepts git commits to record sensitivity signals and mirror the commit
5
+ into .GCC/ so the reasoning audit trail stays in sync with the git history.
6
+
7
+ Entry point: devtorch hooks git-commit <commit_msg_file>
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import re
14
+ import sys
15
+ from pathlib import Path
16
+ from typing import Optional
17
+
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Keyword → concept mapping
21
+ # ---------------------------------------------------------------------------
22
+
23
+ _CONCEPT_KEYWORDS: dict[str, list[str]] = {
24
+ "auth": ["auth", "login", "token", "oauth", "jwt", "session", "credential"],
25
+ "schema": ["schema", "migration", "alter", "column", "table", "model"],
26
+ "api": ["api", "endpoint", "route", "rest", "graphql", "grpc", "http"],
27
+ "deploy": ["deploy", "release", "publish", "rollout", "pipeline", "ci", "cd"],
28
+ "security": ["security", "vuln", "cve", "exploit", "xss", "sql", "injection", "sanitize"],
29
+ "performance": ["perf", "performance", "latency", "throughput", "cache", "index", "optimize"],
30
+ "config": ["config", "env", "setting", "feature-flag", "flag", "yaml", "toml"],
31
+ }
32
+
33
+
34
+ def _infer_concept(message: str) -> str:
35
+ """Return the best matching concept keyword for a commit message."""
36
+ # Strip DevTorch auto-capture prefix "[session=<UUID>] auto(conf=...):"
37
+ # and standard git trailers before keyword matching to avoid false positives
38
+ # ("auth" matching "Co-Authored-By", "session" matching "[session=UUID]").
39
+ cleaned = re.sub(r"^\[session=[^\]]+\]\s*auto\([^)]*\):[^\n]*\n?", "", message)
40
+ trailer_re = re.compile(
41
+ r"^(co-authored-by|signed-off-by|reviewed-by|acked-by|reported-by):.*$",
42
+ re.IGNORECASE | re.MULTILINE,
43
+ )
44
+ cleaned = trailer_re.sub("", cleaned)
45
+ lower = cleaned.lower()
46
+ for concept, keywords in _CONCEPT_KEYWORDS.items():
47
+ for kw in keywords:
48
+ if re.search(r"\b" + re.escape(kw) + r"\b", lower):
49
+ return concept
50
+ return "general"
51
+
52
+
53
+ def _word_count(text: str) -> int:
54
+ return len(text.split())
55
+
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # Public API
59
+ # ---------------------------------------------------------------------------
60
+
61
+
62
+ def extract_sensitivity_from_commit(message: str) -> Optional[dict]:
63
+ """
64
+ Infer sensitivity signal from a git commit message.
65
+
66
+ Confidence tiers by message quality:
67
+ - > 50 words: 0.75 (substantive reasoning)
68
+ - > 20 words: 0.70 (reasonable explanation)
69
+ - > 5 words: 0.50 (brief note)
70
+ - <= 5 words: 0.40 (minimal — "fix bug", "wip")
71
+
72
+ Returns {"concept": str, "signal": str, "confidence": float}
73
+ or None if message is empty.
74
+ """
75
+ stripped = message.strip()
76
+ if not stripped:
77
+ return None
78
+
79
+ n_words = _word_count(stripped)
80
+ if n_words > 50:
81
+ confidence = 0.75
82
+ elif n_words > 20:
83
+ confidence = 0.70
84
+ elif n_words > 5:
85
+ confidence = 0.50
86
+ else:
87
+ confidence = 0.40
88
+
89
+ concept = _infer_concept(stripped)
90
+
91
+ return {
92
+ "concept": concept,
93
+ "signal": stripped,
94
+ "confidence": confidence,
95
+ }
96
+
97
+
98
+ def _find_gcc_repo() -> Optional[object]:
99
+ """Walk upward from cwd to find an initialised .GCC/ repository."""
100
+ from devtorch_core import GCCRepository
101
+
102
+ for parent in [Path.cwd()] + list(Path.cwd().parents):
103
+ r = GCCRepository.at(parent)
104
+ if r.is_initialized():
105
+ return r
106
+ return None
107
+
108
+
109
+ def handle_commit_msg(commit_msg_file: str) -> None:
110
+ """
111
+ Git commit-msg hook entry point.
112
+
113
+ Reads commit message from commit_msg_file.
114
+ Finds .GCC/ by walking up from cwd.
115
+ Creates SensitivityEvent and calls repo.add_sensitivity().
116
+ Also calls repo.commit(message=git_message).
117
+ All errors are silent — never interfere with git commit.
118
+ """
119
+ try:
120
+ git_message = Path(commit_msg_file).read_text(encoding="utf-8").strip()
121
+ if not git_message:
122
+ return
123
+
124
+ sensitivity_data = extract_sensitivity_from_commit(git_message)
125
+ if sensitivity_data is None:
126
+ return
127
+
128
+ repo = _find_gcc_repo()
129
+ if repo is None:
130
+ return
131
+
132
+ from devtorch_core.sensitivity import SensitivityEvent
133
+
134
+ ev = SensitivityEvent(
135
+ source_node="git-commit-hook",
136
+ target_concept=sensitivity_data["concept"],
137
+ counterfactuals=sensitivity_data["signal"],
138
+ confidence=sensitivity_data["confidence"],
139
+ disclosure_level="PROTECTED",
140
+ message=git_message,
141
+ )
142
+ repo.add_sensitivity(ev)
143
+ repo.commit(message=git_message)
144
+
145
+ except Exception:
146
+ # Never interfere with git commit
147
+ pass
148
+
149
+
150
+ def install_hook(repo_path: Optional[Path] = None) -> None:
151
+ """
152
+ Install the git commit-msg hook in .git/hooks/commit-msg.
153
+
154
+ Creates a shell script that calls: devtorch hooks git-commit "$1"
155
+ Makes it executable.
156
+ """
157
+ if repo_path is None:
158
+ repo_path = Path.cwd()
159
+
160
+ hooks_dir = repo_path / ".git" / "hooks"
161
+ if not hooks_dir.is_dir():
162
+ raise FileNotFoundError(
163
+ f"No .git/hooks/ directory found at {repo_path}. "
164
+ "Are you inside a git repository?"
165
+ )
166
+
167
+ hook_file = hooks_dir / "commit-msg"
168
+ hook_script = "#!/bin/sh\ndevtorch hooks git-commit \"$1\"\n"
169
+ hook_file.write_text(hook_script, encoding="utf-8")
170
+ hook_file.chmod(0o755)
171
+
172
+
173
+ def main() -> None:
174
+ """Entry point: devtorch hooks git-commit <commit_msg_file>"""
175
+ if len(sys.argv) < 2:
176
+ sys.stderr.write(
177
+ "Usage: devtorch hooks git-commit <commit_msg_file>\n"
178
+ )
179
+ sys.exit(1)
180
+
181
+ commit_msg_file = sys.argv[1]
182
+ handle_commit_msg(commit_msg_file)