devtorch-core 3.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- devtorch_core/__init__.py +158 -0
- devtorch_core/aggphi_textual.py +275 -0
- devtorch_core/alerts/__init__.py +23 -0
- devtorch_core/alerts/base.py +46 -0
- devtorch_core/alerts/config.py +60 -0
- devtorch_core/alerts/dispatcher.py +110 -0
- devtorch_core/alerts/jira.py +96 -0
- devtorch_core/alerts/linear.py +72 -0
- devtorch_core/alerts/pagerduty.py +66 -0
- devtorch_core/alerts/slack.py +81 -0
- devtorch_core/alerts/teams.py +70 -0
- devtorch_core/audit/__init__.py +43 -0
- devtorch_core/audit/exporter.py +297 -0
- devtorch_core/audit/privacy.py +101 -0
- devtorch_core/audit/scrubber.py +149 -0
- devtorch_core/audit/service.py +67 -0
- devtorch_core/audit/signing.py +127 -0
- devtorch_core/broadcast/__init__.py +4 -0
- devtorch_core/broadcast/broadcaster.py +100 -0
- devtorch_core/broadcast/watcher.py +71 -0
- devtorch_core/capability.py +639 -0
- devtorch_core/cloud/__init__.py +1 -0
- devtorch_core/cloud/client_config.py +472 -0
- devtorch_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
- devtorch_core/cloud/client_configs/.claude-stdio.json +13 -0
- devtorch_core/cloud/client_configs/.cursor-mcp.json +13 -0
- devtorch_core/cloud/client_configs/.opencode-bridge.json +13 -0
- devtorch_core/cloud/client_configs/.opencode.json +15 -0
- devtorch_core/cloud/client_configs/.vscode-mcp.json +13 -0
- devtorch_core/cloud/devtorch-mcp-bridge.js +357 -0
- devtorch_core/cloud/mcp_client.py +229 -0
- devtorch_core/cloud/setup.py +144 -0
- devtorch_core/cloud/sync.py +143 -0
- devtorch_core/cloud/sync_bundle.py +603 -0
- devtorch_core/cloud/sync_conflicts.py +159 -0
- devtorch_core/cloud/sync_state.py +159 -0
- devtorch_core/cloud/team_sync.py +283 -0
- devtorch_core/codex/__init__.py +9 -0
- devtorch_core/codex/__main__.py +97 -0
- devtorch_core/codex/capture.py +208 -0
- devtorch_core/codex/proxy.py +412 -0
- devtorch_core/concept_catalog.py +209 -0
- devtorch_core/consolidation/__init__.py +3 -0
- devtorch_core/consolidation/synthesizer.py +87 -0
- devtorch_core/consolidation/workflow.py +175 -0
- devtorch_core/daemon/__init__.py +27 -0
- devtorch_core/daemon/supervisor.py +293 -0
- devtorch_core/daemon/watcher.py +244 -0
- devtorch_core/dashboard_api.py +2012 -0
- devtorch_core/deltaf.py +97 -0
- devtorch_core/disclosure.py +50 -0
- devtorch_core/divergence/__init__.py +3 -0
- devtorch_core/divergence/detector.py +166 -0
- devtorch_core/gateway/__init__.py +32 -0
- devtorch_core/gateway/key_manager.py +124 -0
- devtorch_core/gateway/metrics_webhook.py +252 -0
- devtorch_core/gateway/policy.py +262 -0
- devtorch_core/gateway/server.py +727 -0
- devtorch_core/gateway/sso.py +233 -0
- devtorch_core/gcc.py +1246 -0
- devtorch_core/github/__init__.py +35 -0
- devtorch_core/github/app.py +240 -0
- devtorch_core/github/comment_builder.py +113 -0
- devtorch_core/github/pat.py +76 -0
- devtorch_core/github/pr_parser.py +82 -0
- devtorch_core/github/pr_reporter.py +555 -0
- devtorch_core/gitlab/__init__.py +177 -0
- devtorch_core/hitl/__init__.py +4 -0
- devtorch_core/hitl/channels.py +129 -0
- devtorch_core/hitl/orchestrator.py +95 -0
- devtorch_core/hooks/__init__.py +17 -0
- devtorch_core/hooks/claude_code.py +228 -0
- devtorch_core/hooks/git_capture.py +341 -0
- devtorch_core/hooks/git_commit.py +182 -0
- devtorch_core/hooks/installer.py +733 -0
- devtorch_core/hooks/pre_commit.py +157 -0
- devtorch_core/hooks/runner.py +344 -0
- devtorch_core/identity/__init__.py +4 -0
- devtorch_core/identity/agent.py +86 -0
- devtorch_core/identity/providers.py +85 -0
- devtorch_core/invariants.py +182 -0
- devtorch_core/mcp/__init__.py +10 -0
- devtorch_core/mcp/auth.py +177 -0
- devtorch_core/mcp/server.py +1049 -0
- devtorch_core/metrics/__init__.py +35 -0
- devtorch_core/metrics/aggregate.py +215 -0
- devtorch_core/metrics/calibrate.py +198 -0
- devtorch_core/metrics/calibration.py +125 -0
- devtorch_core/metrics/credibility.py +288 -0
- devtorch_core/metrics/delivery_time.py +70 -0
- devtorch_core/metrics/dhs.py +126 -0
- devtorch_core/metrics/mcs.py +96 -0
- devtorch_core/metrics/roi.py +88 -0
- devtorch_core/metrics/session_writer.py +81 -0
- devtorch_core/metrics/shadow_ai.py +117 -0
- devtorch_core/metrics/sprint_writer.py +243 -0
- devtorch_core/observability/__init__.py +78 -0
- devtorch_core/observability/datadog.py +157 -0
- devtorch_core/observability/formatter.py +119 -0
- devtorch_core/observability/report.py +264 -0
- devtorch_core/observability/servicenow.py +147 -0
- devtorch_core/observability/splunk.py +218 -0
- devtorch_core/observability/webhook.py +227 -0
- devtorch_core/parser/__init__.py +30 -0
- devtorch_core/parser/blocks.py +216 -0
- devtorch_core/parser/inference.py +159 -0
- devtorch_core/parser/thinking.py +112 -0
- devtorch_core/projects.py +169 -0
- devtorch_core/prompt_artifact.py +76 -0
- devtorch_core/proxy/__init__.py +9 -0
- devtorch_core/proxy/routes/__init__.py +1 -0
- devtorch_core/proxy/routes/anthropic.py +264 -0
- devtorch_core/proxy/routes/azure_openai.py +336 -0
- devtorch_core/proxy/routes/gemini.py +331 -0
- devtorch_core/proxy/routes/groq.py +284 -0
- devtorch_core/proxy/routes/ollama.py +279 -0
- devtorch_core/proxy/routes/openai.py +287 -0
- devtorch_core/proxy/server.py +356 -0
- devtorch_core/query/__init__.py +15 -0
- devtorch_core/query/grep.py +181 -0
- devtorch_core/query/hybrid.py +86 -0
- devtorch_core/query/semantic.py +157 -0
- devtorch_core/rdp.py +105 -0
- devtorch_core/reasoning/__init__.py +4 -0
- devtorch_core/reasoning/entry.py +31 -0
- devtorch_core/reasoning/store.py +122 -0
- devtorch_core/reasoning_plus/__init__.py +70 -0
- devtorch_core/reasoning_plus/augmenter.py +326 -0
- devtorch_core/reasoning_plus/capture.py +51 -0
- devtorch_core/reasoning_plus/config.py +256 -0
- devtorch_core/reasoning_plus/context.py +262 -0
- devtorch_core/reasoning_plus/learning/__init__.py +72 -0
- devtorch_core/reasoning_plus/learning/analytics.py +141 -0
- devtorch_core/reasoning_plus/learning/api.py +313 -0
- devtorch_core/reasoning_plus/learning/chain.py +285 -0
- devtorch_core/reasoning_plus/learning/composer.py +74 -0
- devtorch_core/reasoning_plus/learning/cross_project.py +234 -0
- devtorch_core/reasoning_plus/learning/embeddings.py +209 -0
- devtorch_core/reasoning_plus/learning/extractor.py +207 -0
- devtorch_core/reasoning_plus/learning/models.py +116 -0
- devtorch_core/reasoning_plus/learning/provenance.py +126 -0
- devtorch_core/reasoning_plus/learning/recorder.py +81 -0
- devtorch_core/reasoning_plus/learning/relevance.py +122 -0
- devtorch_core/reasoning_plus/learning/state.py +86 -0
- devtorch_core/reasoning_plus/learning/store.py +160 -0
- devtorch_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
- devtorch_core/reasoning_plus/prompt.py +90 -0
- devtorch_core/rep.py +134 -0
- devtorch_core/rep_network/__init__.py +25 -0
- devtorch_core/rep_network/merge.py +70 -0
- devtorch_core/rep_network/node.py +137 -0
- devtorch_core/rep_network/server.py +140 -0
- devtorch_core/rep_network/sync.py +207 -0
- devtorch_core/sensitivity.py +182 -0
- devtorch_core/serve.py +258 -0
- devtorch_core/session/__init__.py +39 -0
- devtorch_core/session/disagreement.py +188 -0
- devtorch_core/session/models.py +114 -0
- devtorch_core/session/orchestrator.py +182 -0
- devtorch_core/session/planner.py +169 -0
- devtorch_core/session/simulator.py +132 -0
- devtorch_core/signing.py +290 -0
- devtorch_core/sis.py +197 -0
- devtorch_core/storage.py +308 -0
- devtorch_core/templates/__init__.py +6 -0
- devtorch_core/templates/engine.py +122 -0
- devtorch_core/templates/go.py +18 -0
- devtorch_core/templates/infra.py +19 -0
- devtorch_core/templates/library/__init__.py +18 -0
- devtorch_core/templates/library/api_design.md +27 -0
- devtorch_core/templates/library/bug_fix.md +27 -0
- devtorch_core/templates/library/decision_record.md +27 -0
- devtorch_core/templates/library/engine.py +228 -0
- devtorch_core/templates/library/security_review.md +30 -0
- devtorch_core/templates/python.py +19 -0
- devtorch_core/templates/react.py +18 -0
- devtorch_core/templates/typescript.py +18 -0
- devtorch_core/theta.py +221 -0
- devtorch_core/theta_synthesis.py +268 -0
- devtorch_core/topics.py +320 -0
- devtorch_core/variance.py +219 -0
- devtorch_core/wrapper/__init__.py +52 -0
- devtorch_core/wrapper/anthropic.py +487 -0
- devtorch_core/wrapper/base.py +562 -0
- devtorch_core/wrapper/bedrock.py +342 -0
- devtorch_core/wrapper/gemini.py +422 -0
- devtorch_core/wrapper/ollama.py +527 -0
- devtorch_core/wrapper/openai.py +461 -0
- devtorch_core-3.0.1.dist-info/METADATA +867 -0
- devtorch_core-3.0.1.dist-info/RECORD +193 -0
- devtorch_core-3.0.1.dist-info/WHEEL +5 -0
- devtorch_core-3.0.1.dist-info/entry_points.txt +2 -0
- devtorch_core-3.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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 []
|