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,262 @@
1
+ """
2
+ devtorch_core.reasoning_plus.context
3
+ ====================================
4
+ Generic smart workspace context retrieval.
5
+
6
+ Lifts and generalizes the helpers originally in benchmarks/swe_bench/context_utils.py
7
+ so they can be used by any DevTorch wrapper, not just the SWE-Bench runner.
8
+
9
+ The functions are defensive: if `git` is missing, the directory is not a repo, or a
10
+ subprocess call fails, they return empty results rather than raising.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import re
15
+ import subprocess
16
+ from collections import Counter
17
+ from pathlib import Path
18
+ from typing import Iterable
19
+
20
+ # Common English words to ignore when extracting keywords from user prompt text.
21
+ _STOPWORDS = {
22
+ "a", "an", "the", "and", "or", "but", "is", "are", "was", "were",
23
+ "be", "been", "being", "to", "of", "in", "for", "on", "at", "by",
24
+ "with", "from", "as", "it", "its", "this", "that", "these", "those",
25
+ "i", "you", "he", "she", "we", "they", "my", "your", "his", "her",
26
+ "our", "their", "will", "would", "could", "should", "may", "might",
27
+ "can", "must", "shall", "have", "has", "had", "do", "does", "did",
28
+ "not", "no", "yes", "if", "then", "else", "when", "where", "why",
29
+ "how", "what", "which", "who", "whom", "whose", "all", "any", "both",
30
+ "each", "every", "few", "more", "most", "other", "some", "such",
31
+ "only", "own", "same", "so", "than", "too", "very", "just", "also",
32
+ "now", "here", "there", "up", "out", "down", "over", "under", "again",
33
+ "further", "once", "about", "before", "after", "above", "below",
34
+ "between", "into", "through", "during", "before", "after", "until",
35
+ "while", "because", "although", "though", "unless", "since", "although",
36
+ "however", "therefore", "thus", "hence", "moreover", "furthermore",
37
+ "nevertheless", "nonetheless", "otherwise", "instead", "meanwhile",
38
+ "otherwise", "nevertheless", "nonetheless", "additionally", "consequently",
39
+ }
40
+
41
+ # Default file patterns to search. Overrideable by callers.
42
+ DEFAULT_INCLUDE_GLOBS = ("*.py", "*.js", "*.ts", "*.tsx", "*.java", "*.kt", "*.go",
43
+ "*.rs", "*.cpp", "*.c", "*.h", "*.hpp", "*.md", "*.yaml",
44
+ "*.yml", "*.json", "*.toml")
45
+
46
+ # Default exclude globs. Overrideable by callers.
47
+ DEFAULT_EXCLUDE_GLOBS = ("*test*", "*tests*", "*__pycache__*", "*.min.js",
48
+ "*.bundle.js", "node_modules/*", "dist/*", "build/*",
49
+ "*.lock", "*.svg", "*.png", "*.jpg")
50
+
51
+
52
+ def extract_keywords(text: str, max_keywords: int = 15) -> list[str]:
53
+ """
54
+ Extract technical keywords from prompt text.
55
+
56
+ Strategy:
57
+ - Keep code-like identifiers: snake_case, camelCase, dotted.module.paths.
58
+ - Keep words that contain digits or non-alphabetic characters.
59
+ - Drop common English stopwords and very short words.
60
+ - Return the most frequent technical terms, deduplicated.
61
+ """
62
+ if not text:
63
+ return []
64
+
65
+ tokens = re.findall(r"[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*", text)
66
+
67
+ candidates = []
68
+ for tok in tokens:
69
+ t = tok.lower()
70
+ if len(t) < 2:
71
+ continue
72
+ if t in _STOPWORDS:
73
+ continue
74
+ if (
75
+ "_" in t
76
+ or any(c.isdigit() for c in t)
77
+ or any(c.isupper() for c in tok)
78
+ or "." in t
79
+ ):
80
+ candidates.append(t)
81
+ elif len(t) > 4:
82
+ candidates.append(t)
83
+
84
+ counts = Counter(candidates)
85
+ ranked = [t for t, _ in counts.most_common(max_keywords * 2)]
86
+ seen: set[str] = set()
87
+ out: list[str] = []
88
+ for t in ranked:
89
+ if t not in seen:
90
+ seen.add(t)
91
+ out.append(t)
92
+ if len(out) >= max_keywords:
93
+ break
94
+ return out
95
+
96
+
97
+ def search_repo(
98
+ repo_path: Path,
99
+ keywords: list[str],
100
+ base_commit: str | None = None,
101
+ top_n: int = 3,
102
+ max_matches_per_keyword: int = 20,
103
+ include_globs: Iterable[str] = (),
104
+ exclude_globs: Iterable[str] = (),
105
+ ) -> list[str]:
106
+ """
107
+ Search a git repo for files related to the given keywords.
108
+
109
+ Returns up to `top_n` file paths ranked by keyword hit count.
110
+ Returns empty list if the path is not a git repo or `git` is unavailable.
111
+ """
112
+ if not keywords or not repo_path.exists():
113
+ return []
114
+
115
+ include_globs = tuple(include_globs) or DEFAULT_INCLUDE_GLOBS
116
+ exclude_globs = tuple(exclude_globs) or DEFAULT_EXCLUDE_GLOBS
117
+
118
+ file_scores: Counter[str] = Counter()
119
+
120
+ for kw in keywords:
121
+ try:
122
+ pathspecs = ["--", *include_globs]
123
+ cmd = [
124
+ "git", "grep", "-l", "-i", "-E", re.escape(kw),
125
+ *pathspecs,
126
+ ]
127
+ if base_commit:
128
+ cmd = ["git", "grep", "-l", "-i", "-E", re.escape(kw), base_commit, "--", *include_globs]
129
+
130
+ result = subprocess.run(
131
+ cmd,
132
+ cwd=repo_path,
133
+ capture_output=True,
134
+ text=True,
135
+ errors="ignore",
136
+ timeout=30,
137
+ )
138
+ if result.returncode != 0:
139
+ continue
140
+ files = result.stdout.strip().splitlines()
141
+ for fp in files[:max_matches_per_keyword]:
142
+ if _is_excluded(fp, exclude_globs):
143
+ continue
144
+ file_scores[fp] += 1
145
+ except Exception:
146
+ continue
147
+
148
+ ranked = [fp for fp, _ in file_scores.most_common()]
149
+ return ranked[:top_n]
150
+
151
+
152
+ def read_files_at_commit(
153
+ repo_path: Path,
154
+ file_paths: list[str],
155
+ commit: str | None = None,
156
+ max_lines: int = 150,
157
+ ) -> str:
158
+ """
159
+ Read file contents at a specific commit, capped per file.
160
+
161
+ If `commit` is None, reads from the working tree.
162
+ """
163
+ chunks = []
164
+ for fp in file_paths:
165
+ try:
166
+ if commit:
167
+ result = subprocess.run(
168
+ ["git", "show", f"{commit}:{fp}"],
169
+ cwd=repo_path,
170
+ capture_output=True,
171
+ text=True,
172
+ errors="ignore",
173
+ timeout=10,
174
+ )
175
+ if result.returncode != 0:
176
+ continue
177
+ text = result.stdout
178
+ else:
179
+ target = repo_path / fp
180
+ if not target.exists():
181
+ continue
182
+ text = target.read_text(encoding="utf-8", errors="ignore")
183
+ except Exception:
184
+ continue
185
+
186
+ lines = text.splitlines()
187
+ if len(lines) > max_lines:
188
+ text = "\n".join(lines[:max_lines]) + f"\n\n... [{len(lines) - max_lines} lines truncated]\n"
189
+ chunks.append(f"### File: {fp}\n```\n{text}\n```\n")
190
+ return "\n".join(chunks)
191
+
192
+
193
+ def gather_smart_context(
194
+ repo_path: Path | str,
195
+ prompt_text: str,
196
+ *,
197
+ base_commit: str | None = None,
198
+ top_n: int = 3,
199
+ max_lines: int = 150,
200
+ max_keywords: int = 15,
201
+ include_globs: Iterable[str] = (),
202
+ exclude_globs: Iterable[str] = (),
203
+ ) -> tuple[list[str], str]:
204
+ """
205
+ End-to-end smart context: extract keywords, search repo, read files.
206
+
207
+ Returns a tuple of (file_paths, contents_markdown).
208
+ """
209
+ repo_path = Path(repo_path)
210
+ if not _is_git_repo(repo_path):
211
+ return [], ""
212
+
213
+ keywords = extract_keywords(prompt_text, max_keywords=max_keywords)
214
+ if not keywords:
215
+ return [], ""
216
+
217
+ extra_paths = search_repo(
218
+ repo_path,
219
+ keywords,
220
+ base_commit=base_commit,
221
+ top_n=top_n,
222
+ include_globs=include_globs,
223
+ exclude_globs=exclude_globs,
224
+ )
225
+ if not extra_paths:
226
+ return [], ""
227
+
228
+ contents = read_files_at_commit(
229
+ repo_path,
230
+ extra_paths,
231
+ commit=base_commit,
232
+ max_lines=max_lines,
233
+ )
234
+ return extra_paths, contents
235
+
236
+
237
+ def _is_git_repo(path: Path) -> bool:
238
+ """Return True if `path` is inside a git repository."""
239
+ try:
240
+ result = subprocess.run(
241
+ ["git", "rev-parse", "--git-dir"],
242
+ cwd=path,
243
+ capture_output=True,
244
+ text=True,
245
+ errors="ignore",
246
+ timeout=5,
247
+ )
248
+ return result.returncode == 0 and result.stdout.strip() != ""
249
+ except Exception:
250
+ return False
251
+
252
+
253
+ def _is_excluded(file_path: str, exclude_globs: Iterable[str]) -> bool:
254
+ """Simple glob check; any glob that is a substring/endswith match."""
255
+ for g in exclude_globs:
256
+ if g.endswith("*"):
257
+ prefix = g[:-1]
258
+ if file_path.startswith(prefix) or f"/{prefix}" in file_path:
259
+ return True
260
+ if g in file_path:
261
+ return True
262
+ return False
@@ -0,0 +1,72 @@
1
+ """
2
+ devtorch_core.reasoning_plus.learning
3
+ =====================================
4
+ Reasoning Plus Learning (DRPL): capture, distill, and reuse reasoning
5
+ across LLM, tool, and memory calls.
6
+
7
+ Public API:
8
+ ReasoningPlusLearning — high-level facade
9
+ ReasoningCall — dataclass for a recorded call
10
+ Learning — dataclass for a distilled learning
11
+ CallRecorder — persist calls to .GCC/
12
+ LearningStore — persist learnings to .GCC/
13
+ LearningExtractor — derive learnings from calls
14
+ RelevanceEngine — rank learnings by relevance
15
+ PromptComposer — format learnings for prompts
16
+ workspace_state_hash — compute a state hash for staleness
17
+ cross_project_suggest — suggest learnings from sibling governed projects
18
+ register_project — register a project for cross-project learning
19
+ list_projects — list registered governed projects
20
+ """
21
+ from __future__ import annotations
22
+
23
+ from devtorch_core.reasoning_plus.learning.api import ReasoningPlusLearning
24
+ from devtorch_core.reasoning_plus.learning.composer import PromptComposer
25
+ from devtorch_core.reasoning_plus.learning.cross_project import (
26
+ CrossProjectRegistry,
27
+ GovernedProject,
28
+ cross_project_suggest,
29
+ list_projects,
30
+ register_project,
31
+ )
32
+ from devtorch_core.reasoning_plus.learning.extractor import LearningExtractor
33
+ from devtorch_core.reasoning_plus.learning.models import Learning, ReasoningCall
34
+ from devtorch_core.reasoning_plus.learning.recorder import CallRecorder
35
+ from devtorch_core.reasoning_plus.learning.relevance import RelevanceEngine
36
+ from devtorch_core.reasoning_plus.learning.state import workspace_state_hash
37
+ from devtorch_core.reasoning_plus.learning.store import LearningStore
38
+ from devtorch_core.reasoning_plus.learning.analytics import (
39
+ concept_stats,
40
+ danger_zones,
41
+ concept_health_section,
42
+ )
43
+ from devtorch_core.reasoning_plus.learning.theta_learning_bridge import (
44
+ sync_learnings_to_theta,
45
+ )
46
+ from devtorch_core.reasoning_plus.learning.chain import (
47
+ build_chain,
48
+ format_chains,
49
+ )
50
+
51
+ __all__ = [
52
+ "ReasoningPlusLearning",
53
+ "ReasoningCall",
54
+ "Learning",
55
+ "CallRecorder",
56
+ "LearningStore",
57
+ "LearningExtractor",
58
+ "RelevanceEngine",
59
+ "PromptComposer",
60
+ "workspace_state_hash",
61
+ "cross_project_suggest",
62
+ "register_project",
63
+ "list_projects",
64
+ "CrossProjectRegistry",
65
+ "GovernedProject",
66
+ "concept_stats",
67
+ "danger_zones",
68
+ "concept_health_section",
69
+ "sync_learnings_to_theta",
70
+ "build_chain",
71
+ "format_chains",
72
+ ]
@@ -0,0 +1,141 @@
1
+ """
2
+ devtorch_core.reasoning_plus.learning.analytics
3
+ ================================================
4
+ Per-concept learning analytics: stats, danger-zone detection, health dashboard.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ from collections import Counter
10
+ from typing import Any
11
+
12
+ from devtorch_core.reasoning_plus.learning.store import LearningStore
13
+
14
+ logger = logging.getLogger("devtorch.reasoning_plus.learning")
15
+
16
+
17
+ def concept_stats(store: LearningStore) -> dict[str, dict[str, Any]]:
18
+ """Return per-concept statistics aggregated from all stored learnings.
19
+
20
+ Returns a dict keyed by lowercased concept name with:
21
+ - total: total learnings tagged with this concept
22
+ - active: learnings with validity="active"
23
+ - stale: learnings with validity="stale"
24
+ - deprecated: learnings with validity="deprecated"
25
+ - by_type: Counter of learning types (insight, correction, pattern, avoid, confirm)
26
+ - by_outcome: Counter of outcomes among source calls (success, failure, partial, unknown)
27
+ - avg_confidence: mean confidence across all learnings for this concept
28
+ - success_rate: fraction of (non-unknown) outcomes that are "success"
29
+ """
30
+ all_learnings = store.list()
31
+ concept_map: dict[str, dict[str, Any]] = {}
32
+
33
+ for learning in all_learnings:
34
+ concepts = learning.trigger_concepts
35
+ if not concepts:
36
+ concepts = ["__untyped__"]
37
+ for concept in concepts:
38
+ cname = concept.lower()
39
+ if cname not in concept_map:
40
+ concept_map[cname] = {
41
+ "total": 0,
42
+ "active": 0,
43
+ "stale": 0,
44
+ "deprecated": 0,
45
+ "by_type": Counter(),
46
+ "by_outcome": Counter(),
47
+ "confidences": [],
48
+ }
49
+ concept_map[cname]["total"] += 1
50
+ concept_map[cname][learning.validity] += 1
51
+ concept_map[cname]["by_type"][learning.type] += 1
52
+ concept_map[cname]["confidences"].append(learning.confidence)
53
+
54
+ for cname, data in concept_map.items():
55
+ confs = data.pop("confidences")
56
+ data["avg_confidence"] = sum(confs) / len(confs) if confs else 0.0
57
+ data["success_rate"] = _compute_success_rate(store, cname)
58
+
59
+ return dict(sorted(concept_map.items()))
60
+
61
+
62
+ def _compute_success_rate(store: LearningStore, concept: str) -> float:
63
+ """Query call records for the given concept and compute success fraction."""
64
+ from devtorch_core.reasoning_plus.learning.models import ReasoningCall
65
+ from devtorch_core.reasoning_plus.learning.recorder import CallRecorder
66
+
67
+ recorder = CallRecorder(store._gcc_dir)
68
+ calls = recorder.list()
69
+ matched = [c for c in calls if concept in {co.lower() for co in c.concepts}]
70
+ non_unknown = [c for c in matched if c.outcome not in ("unknown", "")]
71
+ if not non_unknown:
72
+ return 0.0
73
+ successes = sum(1 for c in non_unknown if c.outcome == "success")
74
+ return successes / len(non_unknown)
75
+
76
+
77
+ def danger_zones(
78
+ store: LearningStore,
79
+ threshold: float = 0.4,
80
+ min_samples: int = 3,
81
+ ) -> list[dict[str, Any]]:
82
+ """Return concepts with success rate below *threshold*.
83
+
84
+ Each entry contains:
85
+ - concept: concept name
86
+ - success_rate: computed success fraction
87
+ - total_learnings: total learnings for this concept
88
+ - avg_confidence: mean learning confidence
89
+ Only concepts with at least *min_samples* total learnings are considered.
90
+ """
91
+ stats = concept_stats(store)
92
+ zones: list[dict[str, Any]] = []
93
+ for cname, data in stats.items():
94
+ if cname == "__untyped__":
95
+ continue
96
+ if data["total"] < min_samples:
97
+ continue
98
+ if data["success_rate"] < threshold:
99
+ zones.append({
100
+ "concept": cname,
101
+ "success_rate": data["success_rate"],
102
+ "total_learnings": data["total"],
103
+ "avg_confidence": data["avg_confidence"],
104
+ })
105
+ zones.sort(key=lambda x: x["success_rate"])
106
+ return zones
107
+
108
+
109
+ def concept_health_section(store: LearningStore, threshold: float = 0.4) -> str:
110
+ """Return a formatted concept health block for the ``devtorch today`` dashboard."""
111
+ stats = concept_stats(store)
112
+ if not stats:
113
+ return ""
114
+
115
+ lines = ["─── Concept health ───"]
116
+
117
+ zones = danger_zones(store, threshold=threshold)
118
+ if zones:
119
+ lines.append(f" \u26a0 DANGER ZONES (success rate < {threshold:.0%}):")
120
+ for z in zones[:5]:
121
+ lines.append(
122
+ f" {z['concept']}: success {z['success_rate']:.0%}, "
123
+ f"{z['total_learnings']} learning(s), "
124
+ f"avg confidence {z['avg_confidence']:.2f}"
125
+ )
126
+
127
+ healthy = [(c, d) for c, d in stats.items()
128
+ if c != "__untyped__" and d["success_rate"] >= threshold]
129
+ if healthy:
130
+ lines.append(f" Healthy concepts ({len(healthy)}):")
131
+ for c, d in sorted(healthy, key=lambda x: x[1]["success_rate"], reverse=True)[:8]:
132
+ lines.append(
133
+ f" {c}: {d['active']} active, "
134
+ f"{d['success_rate']:.0%} success"
135
+ )
136
+
137
+ untyped = stats.get("__untyped__")
138
+ if untyped and untyped["total"] > 0:
139
+ lines.append(f" Untagged learnings: {untyped['total']}")
140
+
141
+ return "\n".join(lines)