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,158 @@
1
+ from .gcc import (
2
+ GCCRepository,
3
+ GCCIssue,
4
+ GCC_DIR_NAME,
5
+ EVENT_LOG_NAME,
6
+ MAIN_MD_NAME,
7
+ LOG_MD_NAME,
8
+ SENSITIVITIES_DIR_NAME,
9
+ PST_REPORTS_DIR_NAME,
10
+ )
11
+ from .sensitivity import SensitivityEvent, SensitivityStore, DISCLOSURE_LEVELS
12
+ from .theta import AggPhi, RulesBasedAggPhi, ThetaStore, make_theta_store
13
+ from .aggphi_textual import (
14
+ ConfidenceWeightedAggPhi,
15
+ TextualModeAggPhi,
16
+ near_duplicate_pairs,
17
+ merge_i3_records,
18
+ )
19
+ from .capability import (
20
+ OmegaCapability,
21
+ CapabilityStore,
22
+ CapabilityValidationResult,
23
+ PSTRunner,
24
+ PSTReport,
25
+ L_MAX_DEFAULT,
26
+ PST_SCORE_THRESHOLD,
27
+ PST_STATUS_PASSED,
28
+ PST_STATUS_FAILED,
29
+ PST_STATUS_NOT_RUN,
30
+ # S9: A1 round cap
31
+ check_round_cap,
32
+ RoundCapResult,
33
+ T_MAX_MULTIPLIER,
34
+ CONVERGENCE_TIMEOUT_ROUNDS,
35
+ )
36
+ from .invariants import (
37
+ InvariantFailure,
38
+ InvariantContext,
39
+ InvariantEngine,
40
+ ConceptStore,
41
+ check_i1_commit_backed,
42
+ make_i3_semantic_handshake,
43
+ )
44
+ from .variance import (
45
+ f_max,
46
+ VARIANCE_ALERT_EVENT,
47
+ VarianceReport,
48
+ VarianceStore,
49
+ VarianceMonitorResult,
50
+ variance_calibrate,
51
+ run_variance_monitor,
52
+ variance_deterministic_mode_forced,
53
+ reputation_with_variance_penalty,
54
+ )
55
+ from .rep import REPEnvelope, REPLedger, create_envelope
56
+ from .sis import (
57
+ SISTCCorpusEntry,
58
+ SISTCReport,
59
+ load_sis_tc_corpus,
60
+ run_sis_tc_eval,
61
+ QuarantineDecision,
62
+ quarantine_decision_tree,
63
+ QuarantineEventStore,
64
+ )
65
+ from .prompt_artifact import PromptArtifactStore
66
+ from .deltaf import DeltaFReport, DeltaFStore, run_deltaf_estimation
67
+ from .rdp import RDPState, RDPAccountant, PRIVACY_BUDGET_EXHAUSTED_EVENT
68
+ from .disclosure import DisclosurePolicy, apply_disclosure_policy, receiver_mandate_private_count
69
+ from .reasoning import ReasoningEntry, ReasoningStore
70
+
71
+ __all__ = [
72
+ # GCC core
73
+ "GCCRepository",
74
+ "GCCIssue",
75
+ "GCC_DIR_NAME",
76
+ "EVENT_LOG_NAME",
77
+ "MAIN_MD_NAME",
78
+ "LOG_MD_NAME",
79
+ "SENSITIVITIES_DIR_NAME",
80
+ "PST_REPORTS_DIR_NAME",
81
+ # Sensitivity (S3)
82
+ "SensitivityEvent",
83
+ "SensitivityStore",
84
+ "DISCLOSURE_LEVELS",
85
+ # Theta / Aggφ (S3 / S8)
86
+ "AggPhi",
87
+ "RulesBasedAggPhi",
88
+ "ThetaStore",
89
+ "make_theta_store",
90
+ "ConfidenceWeightedAggPhi",
91
+ "TextualModeAggPhi",
92
+ "near_duplicate_pairs",
93
+ "merge_i3_records",
94
+ # Capability / PST (S3)
95
+ "OmegaCapability",
96
+ "CapabilityStore",
97
+ "CapabilityValidationResult",
98
+ "PSTRunner",
99
+ "PSTReport",
100
+ "L_MAX_DEFAULT",
101
+ "PST_SCORE_THRESHOLD",
102
+ "PST_STATUS_PASSED",
103
+ "PST_STATUS_FAILED",
104
+ "PST_STATUS_NOT_RUN",
105
+ # S9: A1 round cap
106
+ "check_round_cap",
107
+ "RoundCapResult",
108
+ "T_MAX_MULTIPLIER",
109
+ "CONVERGENCE_TIMEOUT_ROUNDS",
110
+ # Invariants (S4)
111
+ "InvariantFailure",
112
+ "InvariantContext",
113
+ "InvariantEngine",
114
+ "ConceptStore",
115
+ "check_i1_commit_backed",
116
+ "make_i3_semantic_handshake",
117
+ # Variance (S4)
118
+ "f_max",
119
+ "VARIANCE_ALERT_EVENT",
120
+ "VarianceReport",
121
+ "VarianceStore",
122
+ "VarianceMonitorResult",
123
+ "variance_calibrate",
124
+ "run_variance_monitor",
125
+ "variance_deterministic_mode_forced",
126
+ "reputation_with_variance_penalty",
127
+ # REP (S5)
128
+ "REPEnvelope",
129
+ "REPLedger",
130
+ "create_envelope",
131
+ # SIS (S5)
132
+ "SISTCCorpusEntry",
133
+ "SISTCReport",
134
+ "load_sis_tc_corpus",
135
+ "run_sis_tc_eval",
136
+ "QuarantineDecision",
137
+ "quarantine_decision_tree",
138
+ "QuarantineEventStore",
139
+ # Prompt artifact (S5)
140
+ "PromptArtifactStore",
141
+ # Deltaf (S5)
142
+ "DeltaFReport",
143
+ "DeltaFStore",
144
+ "run_deltaf_estimation",
145
+ # RDP (S5)
146
+ "RDPState",
147
+ "RDPAccountant",
148
+ "PRIVACY_BUDGET_EXHAUSTED_EVENT",
149
+ # Disclosure (S5)
150
+ "DisclosurePolicy",
151
+ "apply_disclosure_policy",
152
+ "receiver_mandate_private_count",
153
+ # Reasoning (S19)
154
+ "ReasoningEntry",
155
+ "ReasoningStore",
156
+ ]
157
+
158
+
@@ -0,0 +1,275 @@
1
+ """
2
+ Sprint 8 — Textual Mode Aggφ (LLM-assisted + confidence-weighted aggregation).
3
+
4
+ Provides:
5
+ - :class:`ConfidenceWeightedAggPhi` — deterministic aggregation: recency weighting
6
+ multiplied by a confidence signal weight (low-confidence events contribute less).
7
+ - :class:`TextualModeAggPhi` — uses confidence-weighted aggregation; optionally
8
+ refines the per-concept delta via Anthropic when ``DEVTORCH_AGGPHI_LLM=1`` and
9
+ credentials are available; otherwise identical to the weighted path.
10
+ - :func:`near_duplicate_pairs` — detects near-duplicate concept names for I3
11
+ handshake (does not merge keys automatically).
12
+
13
+ Environment
14
+ ---------
15
+ DEVTORCH_AGGPHI — ``rules`` (default) | ``textual`` — select aggregator in
16
+ :func:`devtorch_core.theta.make_theta_store`.
17
+ DEVTORCH_AGGPHI_LLM — ``1``/``true`` to enable optional LLM refinement inside
18
+ :class:`TextualModeAggPhi` (falls back on any error).
19
+ DEVTORCH_DISABLE — if set, LLM refinement is skipped.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import logging
26
+ import os
27
+ import re
28
+ from difflib import SequenceMatcher
29
+ from typing import Dict, List, Optional
30
+
31
+ from .theta import AggPhi
32
+
33
+ logger = logging.getLogger("devtorch.aggphi_textual")
34
+
35
+ _LEVEL_RANK: Dict[str, int] = {"PUBLIC": 0, "PROTECTED": 1, "PRIVATE": 2}
36
+ _RANK_LEVEL: Dict[int, str] = {0: "PUBLIC", 1: "PROTECTED", 2: "PRIVATE"}
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Near-duplicate detection (I3 handshake candidates)
40
+ # ---------------------------------------------------------------------------
41
+
42
+
43
+ def near_duplicate_pairs(
44
+ concepts: List[str],
45
+ *,
46
+ threshold: float = 0.82,
47
+ ) -> List[dict]:
48
+ """
49
+ Return pairs of concept names whose normalised string similarity is >=
50
+ *threshold* (SequenceMatcher ratio on lowercased names).
51
+
52
+ Each item:
53
+ ``concept_a``, ``concept_b`` (lexicographic order), ``similarity``,
54
+ ``requires_i3_handshake``: True
55
+ """
56
+ unique = sorted({c for c in concepts if c and str(c).strip()})
57
+ out: List[dict] = []
58
+ seen: set[tuple[str, str]] = set()
59
+ for i, a in enumerate(unique):
60
+ for b in unique[i + 1 :]:
61
+ if a == b:
62
+ continue
63
+ ratio = SequenceMatcher(None, a.lower(), b.lower()).ratio()
64
+ if ratio < threshold:
65
+ continue
66
+ key = (a, b) if a < b else (b, a)
67
+ if key in seen:
68
+ continue
69
+ seen.add(key)
70
+ ca, cb = key
71
+ out.append(
72
+ {
73
+ "concept_a": ca,
74
+ "concept_b": cb,
75
+ "similarity": round(ratio, 4),
76
+ "requires_i3_handshake": True,
77
+ }
78
+ )
79
+ return out
80
+
81
+
82
+ def merge_i3_records(existing: List[dict], incoming: List[dict]) -> List[dict]:
83
+ """Union by unordered pair (concept_a, concept_b); keep highest similarity."""
84
+ by_pair: dict[tuple[str, str], dict] = {}
85
+ for row in existing + incoming:
86
+ a = row.get("concept_a", "")
87
+ b = row.get("concept_b", "")
88
+ if not a or not b:
89
+ continue
90
+ key = tuple(sorted((a, b)))
91
+ prev = by_pair.get(key)
92
+ if prev is None or float(row.get("similarity", 0)) > float(
93
+ prev.get("similarity", 0)
94
+ ):
95
+ by_pair[key] = row
96
+ return sorted(by_pair.values(), key=lambda r: (-r.get("similarity", 0), r["concept_a"]))
97
+
98
+
99
+ # ---------------------------------------------------------------------------
100
+ # Confidence-weighted Aggφ (deterministic)
101
+ # ---------------------------------------------------------------------------
102
+
103
+
104
+ class ConfidenceWeightedAggPhi(AggPhi):
105
+ """
106
+ Like the rules-based aggregator (group by concept, recency 2×/1×), but each
107
+ event's contribution is also scaled by ``max(ε, confidence)**exponent`` so
108
+ inferred/low-confidence signals move Θ less than explicit high-confidence ones.
109
+ """
110
+
111
+ def __init__(self, confidence_exponent: float = 1.5, epsilon: float = 0.05) -> None:
112
+ self.confidence_exponent = confidence_exponent
113
+ self.epsilon = epsilon
114
+
115
+ def aggregate(self, events: List[dict]) -> Dict[str, dict]:
116
+ grouped: Dict[str, List[dict]] = {}
117
+ for ev in events:
118
+ concept = ev.get("target_concept", "unknown")
119
+ grouped.setdefault(concept, []).append(ev)
120
+
121
+ result: Dict[str, dict] = {}
122
+ for concept, group in grouped.items():
123
+ sorted_group = sorted(
124
+ group,
125
+ key=lambda e: e.get("created_at", ""),
126
+ reverse=True,
127
+ )
128
+ total_w = 0.0
129
+ weighted_conf_sum = 0.0
130
+ max_rank = 0
131
+
132
+ for i, ev in enumerate(sorted_group):
133
+ recency_w = 2.0 if i == 0 else 1.0
134
+ conf = float(ev.get("confidence", 0.5))
135
+ sig = max(self.epsilon, conf) ** self.confidence_exponent
136
+ w = recency_w * sig
137
+ weighted_conf_sum += conf * w
138
+ total_w += w
139
+
140
+ rank = _LEVEL_RANK.get(ev.get("disclosure_level", "PUBLIC"), 0)
141
+ if rank > max_rank:
142
+ max_rank = rank
143
+
144
+ mean_conf = weighted_conf_sum / total_w if total_w else 0.0
145
+ result[concept] = {
146
+ "mean_confidence": round(mean_conf, 6),
147
+ "disclosure_level": _RANK_LEVEL[max_rank],
148
+ "event_count": len(group),
149
+ }
150
+ return result
151
+
152
+
153
+ # ---------------------------------------------------------------------------
154
+ # Optional LLM refinement (structured JSON)
155
+ # ---------------------------------------------------------------------------
156
+
157
+ _LLM_JSON_RE = re.compile(r"\{[\s\S]*\}")
158
+
159
+
160
+ def _try_llm_synthesize_delta(events: List[dict]) -> Optional[Dict[str, dict]]:
161
+ """
162
+ Ask the model to emit a JSON object mapping concept -> {mean_confidence,
163
+ disclosure_level, event_count}. Returns None if unavailable or invalid.
164
+ """
165
+ if os.environ.get("DEVTORCH_DISABLE", "").strip().lower() in ("1", "true", "yes"):
166
+ return None
167
+ if not os.environ.get("ANTHROPIC_API_KEY"):
168
+ return None
169
+ try:
170
+ import anthropic # noqa: PLC0415
171
+ except ImportError:
172
+ return None
173
+
174
+ payload = json.dumps(events, indent=2, sort_keys=True)[:12000]
175
+ prompt = (
176
+ "You are a governance engine aggregating sensitivity events into a "
177
+ "coordination vector Θ. Given the JSON list of events below, output "
178
+ "**only** a single JSON object (no markdown) whose keys are "
179
+ "target_concept strings and values are objects with:\n"
180
+ ' "mean_confidence": float in [0,1],\n'
181
+ ' "disclosure_level": one of PUBLIC, PROTECTED, PRIVATE,\n'
182
+ ' "event_count": positive integer (count of events merged into that concept).\n'
183
+ "Aggregate semantically: same meaning under different wording should map "
184
+ "to one concept key when obvious; otherwise keep distinct keys.\n\n"
185
+ f"EVENTS:\n{payload}"
186
+ )
187
+
188
+ try:
189
+ client = anthropic.Anthropic()
190
+ resp = client.messages.create(
191
+ model=os.environ.get("DEVTORCH_AGGPHI_MODEL", "claude-3-5-haiku-20241022"),
192
+ max_tokens=4096,
193
+ temperature=0.0,
194
+ messages=[{"role": "user", "content": prompt}],
195
+ )
196
+ raw = resp.content[0].text
197
+ except Exception as exc:
198
+ logger.warning("devtorch Aggφ LLM call failed — %s", exc)
199
+ return None
200
+
201
+ m = _LLM_JSON_RE.search(raw)
202
+ if not m:
203
+ logger.warning("devtorch Aggφ LLM: no JSON object in response")
204
+ return None
205
+ try:
206
+ obj = json.loads(m.group(0))
207
+ except json.JSONDecodeError as exc:
208
+ logger.warning("devtorch Aggφ LLM: JSON parse error — %s", exc)
209
+ return None
210
+
211
+ if not isinstance(obj, dict):
212
+ return None
213
+
214
+ out: Dict[str, dict] = {}
215
+ for concept, entry in obj.items():
216
+ if not isinstance(concept, str) or not isinstance(entry, dict):
217
+ continue
218
+ try:
219
+ mc = float(entry.get("mean_confidence", 0.0))
220
+ dl = str(entry.get("disclosure_level", "PUBLIC")).upper()
221
+ ec = int(entry.get("event_count", 1))
222
+ except (TypeError, ValueError):
223
+ continue
224
+ if dl not in _LEVEL_RANK:
225
+ dl = "PUBLIC"
226
+ mc = max(0.0, min(1.0, mc))
227
+ ec = max(1, ec)
228
+ out[concept] = {
229
+ "mean_confidence": round(mc, 6),
230
+ "disclosure_level": dl,
231
+ "event_count": ec,
232
+ }
233
+
234
+ return out if out else None
235
+
236
+
237
+ # ---------------------------------------------------------------------------
238
+ # Textual Mode Aggφ (S8 facade)
239
+ # ---------------------------------------------------------------------------
240
+
241
+
242
+ class TextualModeAggPhi(AggPhi):
243
+ """
244
+ Sprint 8 default: confidence-weighted aggregation, optional LLM refinement,
245
+ and I3 near-duplicate tracking (via :class:`ThetaStore` when this class is used
246
+ as the aggregator).
247
+ """
248
+
249
+ records_i3_merge_candidates = True
250
+ similarity_threshold = 0.82
251
+
252
+ def __init__(
253
+ self,
254
+ *,
255
+ inner: Optional[ConfidenceWeightedAggPhi] = None,
256
+ use_llm: Optional[bool] = None,
257
+ ) -> None:
258
+ self._inner = inner if inner is not None else ConfidenceWeightedAggPhi()
259
+ self._use_llm = use_llm
260
+
261
+ def aggregate(self, events: List[dict]) -> Dict[str, dict]:
262
+ use_llm = self._use_llm
263
+ if use_llm is None:
264
+ use_llm = os.environ.get("DEVTORCH_AGGPHI_LLM", "").strip().lower() in (
265
+ "1",
266
+ "true",
267
+ "yes",
268
+ )
269
+
270
+ if use_llm:
271
+ llm_delta = _try_llm_synthesize_delta(events)
272
+ if llm_delta is not None:
273
+ return llm_delta
274
+
275
+ return self._inner.aggregate(events)
@@ -0,0 +1,23 @@
1
+ from .base import AlertEnvelope, AlertSeverity, DeliveryResult
2
+ from .config import AlertConfig, load_config
3
+ from .dispatcher import dispatch_alert, dispatch_from_event
4
+ from .slack import SlackChannel
5
+ from .teams import MicrosoftTeamsChannel
6
+ from .jira import JiraChannel
7
+ from .linear import LinearChannel
8
+ from .pagerduty import PagerDutyChannel
9
+
10
+ __all__ = [
11
+ "AlertEnvelope",
12
+ "AlertSeverity",
13
+ "DeliveryResult",
14
+ "AlertConfig",
15
+ "dispatch_alert",
16
+ "dispatch_from_event",
17
+ "load_config",
18
+ "SlackChannel",
19
+ "MicrosoftTeamsChannel",
20
+ "JiraChannel",
21
+ "LinearChannel",
22
+ "PagerDutyChannel",
23
+ ]
@@ -0,0 +1,46 @@
1
+ from dataclasses import dataclass, field
2
+ from enum import Enum
3
+
4
+
5
+ class AlertSeverity(Enum):
6
+ INFO = "info"
7
+ WARNING = "warning"
8
+ CRITICAL = "critical"
9
+
10
+
11
+ @dataclass
12
+ class AlertEnvelope:
13
+ event_type: str # e.g. "VARIANCE_ALERT"
14
+ severity: AlertSeverity
15
+ title: str
16
+ body: str # markdown-safe text
17
+ branch: str
18
+ commit_id: str
19
+ metadata: dict = field(default_factory=dict)
20
+
21
+
22
+ @dataclass
23
+ class DeliveryResult:
24
+ channel: str # "slack", "jira", "pagerduty"
25
+ success: bool
26
+ error: str = ""
27
+ response_code: int = 0
28
+
29
+
30
+ class AlertChannel:
31
+ """Base class for alert channels."""
32
+ channel_name: str = "base"
33
+
34
+ def deliver(self, envelope: AlertEnvelope) -> DeliveryResult:
35
+ raise NotImplementedError
36
+
37
+
38
+ def event_to_severity(event_type: str) -> AlertSeverity:
39
+ """Map GCC event types to alert severity."""
40
+ critical = {"PRIVACY_BUDGET_EXHAUSTED", "CAPABILITY_DEGRADED"}
41
+ warning = {"VARIANCE_ALERT", "CONSENSUS_LOCKED"}
42
+ if event_type in critical:
43
+ return AlertSeverity.CRITICAL
44
+ elif event_type in warning:
45
+ return AlertSeverity.WARNING
46
+ return AlertSeverity.INFO
@@ -0,0 +1,60 @@
1
+ import os
2
+ from dataclasses import dataclass
3
+
4
+ from .base import AlertSeverity
5
+
6
+
7
+ @dataclass
8
+ class AlertConfig:
9
+ slack_webhook_url: str = "" # DEVTORCH_SLACK_WEBHOOK_URL
10
+ teams_webhook_url: str = "" # DEVTORCH_TEAMS_WEBHOOK_URL
11
+ jira_url: str = "" # DEVTORCH_JIRA_URL
12
+ jira_project_key: str = "" # DEVTORCH_JIRA_PROJECT_KEY
13
+ jira_api_token: str = "" # DEVTORCH_JIRA_API_TOKEN
14
+ jira_email: str = "" # DEVTORCH_JIRA_EMAIL
15
+ linear_api_token: str = "" # DEVTORCH_LINEAR_API_TOKEN
16
+ linear_team_id: str = "" # DEVTORCH_LINEAR_TEAM_ID
17
+ pagerduty_routing_key: str = "" # DEVTORCH_PAGERDUTY_ROUTING_KEY
18
+ min_severity: AlertSeverity = AlertSeverity.WARNING # DEVTORCH_ALERT_MIN_SEVERITY
19
+
20
+
21
+ def load_config() -> AlertConfig:
22
+ """Load from environment variables. Never raises."""
23
+ try:
24
+ raw_severity = os.environ.get("DEVTORCH_ALERT_MIN_SEVERITY", "warning").lower()
25
+ severity_map = {
26
+ "info": AlertSeverity.INFO,
27
+ "warning": AlertSeverity.WARNING,
28
+ "critical": AlertSeverity.CRITICAL,
29
+ }
30
+ min_severity = severity_map.get(raw_severity, AlertSeverity.WARNING)
31
+
32
+ return AlertConfig(
33
+ slack_webhook_url=os.environ.get("DEVTORCH_SLACK_WEBHOOK_URL", ""),
34
+ teams_webhook_url=os.environ.get("DEVTORCH_TEAMS_WEBHOOK_URL", ""),
35
+ jira_url=os.environ.get("DEVTORCH_JIRA_URL", ""),
36
+ jira_project_key=os.environ.get("DEVTORCH_JIRA_PROJECT_KEY", ""),
37
+ jira_api_token=os.environ.get("DEVTORCH_JIRA_API_TOKEN", ""),
38
+ jira_email=os.environ.get("DEVTORCH_JIRA_EMAIL", ""),
39
+ linear_api_token=os.environ.get("DEVTORCH_LINEAR_API_TOKEN", ""),
40
+ linear_team_id=os.environ.get("DEVTORCH_LINEAR_TEAM_ID", ""),
41
+ pagerduty_routing_key=os.environ.get("DEVTORCH_PAGERDUTY_ROUTING_KEY", ""),
42
+ min_severity=min_severity,
43
+ )
44
+ except Exception:
45
+ return AlertConfig()
46
+
47
+
48
+ def is_channel_configured(config: AlertConfig, channel: str) -> bool:
49
+ """Returns True if the channel has enough config to attempt delivery."""
50
+ if channel == "slack":
51
+ return bool(config.slack_webhook_url)
52
+ if channel == "teams":
53
+ return bool(config.teams_webhook_url)
54
+ if channel == "jira":
55
+ return bool(config.jira_url and config.jira_project_key and config.jira_api_token)
56
+ if channel == "linear":
57
+ return bool(config.linear_api_token and config.linear_team_id)
58
+ if channel == "pagerduty":
59
+ return bool(config.pagerduty_routing_key)
60
+ return False
@@ -0,0 +1,110 @@
1
+ from __future__ import annotations
2
+
3
+ from .base import AlertChannel, AlertEnvelope, AlertSeverity, DeliveryResult, event_to_severity
4
+ from .config import AlertConfig, load_config
5
+ from .jira import JiraChannel
6
+ from .linear import LinearChannel
7
+ from .pagerduty import PagerDutyChannel
8
+ from .slack import SlackChannel
9
+ from .teams import MicrosoftTeamsChannel
10
+
11
+ # Severity ordering for threshold filtering
12
+ _SEVERITY_ORDER = {
13
+ AlertSeverity.INFO: 0,
14
+ AlertSeverity.WARNING: 1,
15
+ AlertSeverity.CRITICAL: 2,
16
+ }
17
+
18
+
19
+ def build_channels(config: AlertConfig) -> list[AlertChannel]:
20
+ """Build the list of configured channels from config."""
21
+ channels: list[AlertChannel] = []
22
+ if config.slack_webhook_url:
23
+ channels.append(SlackChannel(config.slack_webhook_url))
24
+ if config.teams_webhook_url:
25
+ channels.append(MicrosoftTeamsChannel(config.teams_webhook_url))
26
+ if config.jira_url and config.jira_project_key and config.jira_api_token:
27
+ channels.append(
28
+ JiraChannel(
29
+ url=config.jira_url,
30
+ project_key=config.jira_project_key,
31
+ api_token=config.jira_api_token,
32
+ email=config.jira_email,
33
+ )
34
+ )
35
+ if config.linear_api_token and config.linear_team_id:
36
+ channels.append(LinearChannel(config.linear_api_token, config.linear_team_id))
37
+ if config.pagerduty_routing_key:
38
+ channels.append(PagerDutyChannel(config.pagerduty_routing_key))
39
+ return channels
40
+
41
+
42
+ def dispatch_alert(
43
+ envelope: AlertEnvelope,
44
+ config: AlertConfig | None = None,
45
+ ) -> list[DeliveryResult]:
46
+ """
47
+ Route an alert envelope to all configured channels.
48
+ Filters by min_severity. Returns delivery results for all channels.
49
+ Never raises.
50
+ """
51
+ try:
52
+ if config is None:
53
+ config = load_config()
54
+
55
+ # Filter: skip if envelope severity is below the configured minimum
56
+ if _SEVERITY_ORDER.get(envelope.severity, 0) < _SEVERITY_ORDER.get(config.min_severity, 0):
57
+ return []
58
+
59
+ channels = build_channels(config)
60
+ results: list[DeliveryResult] = []
61
+ for channel in channels:
62
+ try:
63
+ result = channel.deliver(envelope)
64
+ results.append(result)
65
+ except Exception as exc:
66
+ results.append(
67
+ DeliveryResult(
68
+ channel=channel.channel_name,
69
+ success=False,
70
+ error=str(exc),
71
+ )
72
+ )
73
+ return results
74
+ except Exception:
75
+ return []
76
+
77
+
78
+ def dispatch_from_event(
79
+ event_type: str,
80
+ branch: str,
81
+ commit_id: str,
82
+ metadata: dict | None = None,
83
+ config: AlertConfig | None = None,
84
+ ) -> list[DeliveryResult]:
85
+ """
86
+ Convenience function: build an AlertEnvelope from a GCC event type
87
+ and dispatch to all channels. Returns [] if no channels configured.
88
+ """
89
+ try:
90
+ if config is None:
91
+ config = load_config()
92
+
93
+ severity = event_to_severity(event_type)
94
+ title = f"{event_type} on {branch}"
95
+ body = (
96
+ f"DevTorch governance event `{event_type}` was triggered on branch `{branch}` "
97
+ f"at commit `{commit_id}`."
98
+ )
99
+ envelope = AlertEnvelope(
100
+ event_type=event_type,
101
+ severity=severity,
102
+ title=title,
103
+ body=body,
104
+ branch=branch,
105
+ commit_id=commit_id,
106
+ metadata=metadata or {},
107
+ )
108
+ return dispatch_alert(envelope, config=config)
109
+ except Exception:
110
+ return []