code-oracle 0.1.0__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.
- code_oracle/__init__.py +30 -0
- code_oracle/cli.py +795 -0
- code_oracle/config.py +145 -0
- code_oracle/dataset.py +5325 -0
- code_oracle/dead_code/__init__.py +32 -0
- code_oracle/dead_code/detector.py +379 -0
- code_oracle/dead_code/entrypoints.py +333 -0
- code_oracle/dead_code/models.py +255 -0
- code_oracle/dead_code/semantics.py +416 -0
- code_oracle/decision.py +906 -0
- code_oracle/engine.py +430 -0
- code_oracle/export_onnx.py +436 -0
- code_oracle/hook.py +531 -0
- code_oracle/indexer.py +894 -0
- code_oracle/languages/__init__.py +114 -0
- code_oracle/languages/common.py +127 -0
- code_oracle/languages/go.py +395 -0
- code_oracle/languages/python.py +336 -0
- code_oracle/languages/rust.py +474 -0
- code_oracle/languages/typescript.py +775 -0
- code_oracle/linearizer.py +166 -0
- code_oracle/locator.py +301 -0
- code_oracle/models.py +237 -0
- code_oracle/perf_lint/__init__.py +38 -0
- code_oracle/perf_lint/engine.py +234 -0
- code_oracle/perf_lint/models.py +229 -0
- code_oracle/perf_lint/rules/__init__.py +31 -0
- code_oracle/perf_lint/rules/async_blocking.py +143 -0
- code_oracle/perf_lint/rules/n_plus_one.py +232 -0
- code_oracle/perf_lint/rules/nested_loops.py +137 -0
- code_oracle/perf_lint/rules/unclosed_res.py +494 -0
- code_oracle/perf_lint/visitor.py +299 -0
- code_oracle/server.py +184 -0
- code_oracle/slicer.py +225 -0
- code_oracle/symbolic.py +459 -0
- code_oracle-0.1.0.dist-info/METADATA +225 -0
- code_oracle-0.1.0.dist-info/RECORD +40 -0
- code_oracle-0.1.0.dist-info/WHEEL +4 -0
- code_oracle-0.1.0.dist-info/entry_points.txt +2 -0
- code_oracle-0.1.0.dist-info/licenses/LICENSE +190 -0
code_oracle/decision.py
ADDED
|
@@ -0,0 +1,906 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Laya ModernBERT Decision Head and Risk Calibration for Code Oracle.
|
|
3
|
+
|
|
4
|
+
Evaluates linearized Micro-DSL subgraphs using fine-tuned Laya weights via ONNX Runtime
|
|
5
|
+
(or PyTorch / legacy fallback) or falls back deterministically to symbolic gate verdicts.
|
|
6
|
+
Supports pure ONNX inference with zero hard dependency on PyTorch at runtime.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import contextlib
|
|
10
|
+
import io
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import math
|
|
14
|
+
import os
|
|
15
|
+
import warnings
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
import numpy as np
|
|
22
|
+
HAS_NUMPY = True
|
|
23
|
+
except ImportError:
|
|
24
|
+
np = None
|
|
25
|
+
HAS_NUMPY = False
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
import torch
|
|
29
|
+
import torch.nn as nn
|
|
30
|
+
import torch.nn.functional as F
|
|
31
|
+
HAS_TORCH = True
|
|
32
|
+
except ImportError:
|
|
33
|
+
torch = None
|
|
34
|
+
nn = None
|
|
35
|
+
F = None
|
|
36
|
+
HAS_TORCH = False
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
import onnxruntime as ort
|
|
40
|
+
HAS_ORT = True
|
|
41
|
+
except ImportError:
|
|
42
|
+
ort = None
|
|
43
|
+
HAS_ORT = False
|
|
44
|
+
|
|
45
|
+
from code_oracle.models import RiskTaxonomyScores
|
|
46
|
+
|
|
47
|
+
logger = logging.getLogger(__name__)
|
|
48
|
+
|
|
49
|
+
# Verification Questions Definition for Laya Typed-Decisions Head
|
|
50
|
+
VERIFICATION_QUESTIONS = {
|
|
51
|
+
"status": {
|
|
52
|
+
"type": "choice",
|
|
53
|
+
"instructions": "Determine if this code patch proposal should be APPROVED or REJECTED based on AST topology, cycles, and contract invariants:",
|
|
54
|
+
"criteria": {
|
|
55
|
+
"APPROVED": "Clean invariant, acyclic call/import topology, parameter contracts valid",
|
|
56
|
+
"REJECTED": "Contains circular dependencies, arity mismatches, unexpected keywords, or deleted symbol references",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
"risk": {
|
|
60
|
+
"type": "score",
|
|
61
|
+
"instructions": "Calibrate the semantic risk of this patch proposal from 0 (completely safe) to 4 (critical breaking change):",
|
|
62
|
+
"criteria": [
|
|
63
|
+
"Level 0: Safe / Invariants Preserved",
|
|
64
|
+
"Level 1: Low Risk / Harmless Additions",
|
|
65
|
+
"Level 2: Moderate Risk / Signature Drift",
|
|
66
|
+
"Level 3: High Risk / Broken Callers",
|
|
67
|
+
"Level 4: Critical / Topological Cycle",
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_ModuleBase = nn.Module if (HAS_TORCH and nn is not None) else object
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class EnhancedDecisionResult:
|
|
77
|
+
"""
|
|
78
|
+
Rich outcome of Laya multi-task neural decision head.
|
|
79
|
+
Supports unpacking as (status, confidence, risk_score) for backward compatibility.
|
|
80
|
+
"""
|
|
81
|
+
status: str
|
|
82
|
+
confidence: float
|
|
83
|
+
risk_score: float
|
|
84
|
+
epistemic_uncertainty: float
|
|
85
|
+
risk_taxonomy: RiskTaxonomyScores
|
|
86
|
+
active_risk_categories: List[str]
|
|
87
|
+
is_neural_calibrated: bool = False
|
|
88
|
+
engine_mode: str = "heuristic"
|
|
89
|
+
|
|
90
|
+
def __iter__(self):
|
|
91
|
+
"""Enable tuple unpacking: status, conf, risk = result"""
|
|
92
|
+
return iter((self.status, self.confidence, self.risk_score))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class ModernBERTMultiTaskModel(_ModuleBase):
|
|
96
|
+
"""
|
|
97
|
+
Hard Parameter Sharing Multi-Task Network over ModernBERT representations.
|
|
98
|
+
Branches from pooled token representation h_pool in R^hidden_size (default 768)
|
|
99
|
+
or optionally couples end-to-end with a ModernBERT encoder:
|
|
100
|
+
- Head 1: Continuous Risk Regression (Huber Loss / Sigmoid)
|
|
101
|
+
- Head 2: Multi-Label Risk Taxonomy (5-Class BCEWithLogits / Sigmoid)
|
|
102
|
+
- Head 3: Epistemic Uncertainty Estimation (Heteroscedastic log-variance with clamp [-6.0, 6.0])
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
TAXONOMY_CLASSES = [
|
|
106
|
+
"BreakingPublicAPI",
|
|
107
|
+
"SecuritySurface",
|
|
108
|
+
"ConcurrencyHazard",
|
|
109
|
+
"PerformanceRegression",
|
|
110
|
+
"SilentLogicDrift",
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
hidden_size_or_encoder: Any = 768,
|
|
116
|
+
num_taxonomy_classes: int = 5,
|
|
117
|
+
**kwargs,
|
|
118
|
+
):
|
|
119
|
+
if not HAS_TORCH:
|
|
120
|
+
raise RuntimeError("PyTorch is required to instantiate ModernBERTMultiTaskModel")
|
|
121
|
+
super().__init__()
|
|
122
|
+
|
|
123
|
+
encoder_name = kwargs.get("encoder_name")
|
|
124
|
+
if isinstance(hidden_size_or_encoder, str):
|
|
125
|
+
encoder_name = hidden_size_or_encoder
|
|
126
|
+
hidden_size = kwargs.get("hidden_size", 768)
|
|
127
|
+
else:
|
|
128
|
+
hidden_size = int(hidden_size_or_encoder)
|
|
129
|
+
|
|
130
|
+
self.encoder_name = encoder_name
|
|
131
|
+
self.encoder = None
|
|
132
|
+
if encoder_name:
|
|
133
|
+
from transformers import AutoModel
|
|
134
|
+
self.encoder = AutoModel.from_pretrained(encoder_name)
|
|
135
|
+
hidden_size = self.encoder.config.hidden_size
|
|
136
|
+
|
|
137
|
+
self.hidden_size = hidden_size
|
|
138
|
+
self.num_taxonomy_classes = num_taxonomy_classes
|
|
139
|
+
|
|
140
|
+
# Head 1: Continuous Risk Regression MLP
|
|
141
|
+
# Dense(hidden_size -> 256) -> GELU -> LayerNorm -> Dense(256 -> 1)
|
|
142
|
+
self.risk_head = nn.Sequential(
|
|
143
|
+
nn.Linear(hidden_size, 256),
|
|
144
|
+
nn.GELU(),
|
|
145
|
+
nn.LayerNorm(256),
|
|
146
|
+
nn.Linear(256, 1),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Head 2: Multi-Label Taxonomy MLP
|
|
150
|
+
# Dense(hidden_size -> 256) -> GELU -> LayerNorm -> Dense(256 -> num_taxonomy_classes)
|
|
151
|
+
self.taxonomy_head = nn.Sequential(
|
|
152
|
+
nn.Linear(hidden_size, 256),
|
|
153
|
+
nn.GELU(),
|
|
154
|
+
nn.LayerNorm(256),
|
|
155
|
+
nn.Linear(256, num_taxonomy_classes),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Head 3: Epistemic Uncertainty MLP
|
|
159
|
+
# Dense(hidden_size -> 128) -> GELU -> Dense(128 -> 1) [log sigma^2]
|
|
160
|
+
self.uncertainty_head = nn.Sequential(
|
|
161
|
+
nn.Linear(hidden_size, 128),
|
|
162
|
+
nn.GELU(),
|
|
163
|
+
nn.Linear(128, 1),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def forward(self, *args, **kwargs) -> Dict[str, Any]:
|
|
167
|
+
"""
|
|
168
|
+
Forward pass. Accepts either:
|
|
169
|
+
- `h_pool: torch.Tensor` (B, hidden_size) directly.
|
|
170
|
+
- `(input_ids, attention_mask)` tensors when initialized with an encoder.
|
|
171
|
+
"""
|
|
172
|
+
if not HAS_TORCH:
|
|
173
|
+
raise RuntimeError("PyTorch is required for model forward pass")
|
|
174
|
+
|
|
175
|
+
if "input_ids" in kwargs:
|
|
176
|
+
input_ids = kwargs["input_ids"]
|
|
177
|
+
attention_mask = kwargs.get("attention_mask")
|
|
178
|
+
if attention_mask is None:
|
|
179
|
+
attention_mask = torch.ones_like(input_ids)
|
|
180
|
+
elif len(args) >= 2 and hasattr(args[0], "dtype") and args[0].dtype in (torch.int32, torch.int64):
|
|
181
|
+
input_ids = args[0]
|
|
182
|
+
attention_mask = args[1]
|
|
183
|
+
elif len(args) == 1 and hasattr(args[0], "dtype") and args[0].dtype in (torch.int32, torch.int64):
|
|
184
|
+
input_ids = args[0]
|
|
185
|
+
attention_mask = torch.ones_like(input_ids)
|
|
186
|
+
elif len(args) == 1:
|
|
187
|
+
h_pool = args[0]
|
|
188
|
+
input_ids = None
|
|
189
|
+
elif "h_pool" in kwargs:
|
|
190
|
+
h_pool = kwargs["h_pool"]
|
|
191
|
+
input_ids = None
|
|
192
|
+
else:
|
|
193
|
+
raise ValueError("Expected either (input_ids, attention_mask) or (h_pool)")
|
|
194
|
+
|
|
195
|
+
if input_ids is not None:
|
|
196
|
+
if self.encoder is None:
|
|
197
|
+
raise RuntimeError("Encoder not loaded. Initialize with encoder_name to accept input_ids.")
|
|
198
|
+
|
|
199
|
+
outputs = self.encoder(input_ids=input_ids, attention_mask=attention_mask)
|
|
200
|
+
token_embeddings = outputs.last_hidden_state
|
|
201
|
+
input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()
|
|
202
|
+
sum_embeddings = torch.sum(token_embeddings * input_mask_expanded, 1)
|
|
203
|
+
sum_mask = torch.clamp(input_mask_expanded.sum(1), min=1e-9)
|
|
204
|
+
h_pool = sum_embeddings / sum_mask
|
|
205
|
+
|
|
206
|
+
# Head 1: Risk score (continuous in 0.0 .. 1.0)
|
|
207
|
+
risk_raw = self.risk_head(h_pool)
|
|
208
|
+
risk_score = torch.sigmoid(risk_raw)
|
|
209
|
+
|
|
210
|
+
# Head 2: Multi-label taxonomy
|
|
211
|
+
taxonomy_logits = self.taxonomy_head(h_pool)
|
|
212
|
+
taxonomy_probs = torch.sigmoid(taxonomy_logits)
|
|
213
|
+
|
|
214
|
+
# Head 3: Epistemic Uncertainty with bounded damping [-6.0, 6.0]
|
|
215
|
+
s = self.uncertainty_head(h_pool)
|
|
216
|
+
log_variance = torch.clamp(s, min=-6.0, max=6.0)
|
|
217
|
+
variance = torch.exp(log_variance)
|
|
218
|
+
sigma = torch.sqrt(variance)
|
|
219
|
+
confidence = 1.0 - torch.clamp(sigma, min=0.0, max=1.0)
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
"risk_score": risk_score,
|
|
223
|
+
"risk_logits": risk_raw,
|
|
224
|
+
"taxonomy_logits": taxonomy_logits,
|
|
225
|
+
"taxonomy_probs": taxonomy_probs,
|
|
226
|
+
"log_variance": log_variance,
|
|
227
|
+
"variance": variance,
|
|
228
|
+
"confidence": confidence,
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
def compute_loss(
|
|
232
|
+
self,
|
|
233
|
+
risk_pred: Any,
|
|
234
|
+
risk_target: Any,
|
|
235
|
+
taxonomy_logits: Any,
|
|
236
|
+
taxonomy_target: Any,
|
|
237
|
+
log_variance: Any,
|
|
238
|
+
delta: float = 0.1,
|
|
239
|
+
pos_weight: Optional[Any] = None,
|
|
240
|
+
risk_pos_weight: float = 1.0,
|
|
241
|
+
homoscedastic_weights: Optional[Tuple[float, float, float]] = None,
|
|
242
|
+
) -> Dict[str, Any]:
|
|
243
|
+
"""
|
|
244
|
+
Compute unified multi-task loss:
|
|
245
|
+
- L_risk: Huber loss (delta=0.1) with optional asymmetric risk weighting (risk_pos_weight)
|
|
246
|
+
- L_tax: BCEWithLogitsLoss (with optional pos_weight)
|
|
247
|
+
- L_unc: Heteroscedastic negative log-likelihood:
|
|
248
|
+
0.5 * exp(-s) * (risk_target - risk_pred)^2 + 0.5 * s
|
|
249
|
+
"""
|
|
250
|
+
if not HAS_TORCH:
|
|
251
|
+
raise RuntimeError("PyTorch is required for compute_loss")
|
|
252
|
+
|
|
253
|
+
risk_pred = risk_pred.view(-1, 1)
|
|
254
|
+
risk_target = risk_target.view(-1, 1).float()
|
|
255
|
+
log_variance = log_variance.view(-1, 1)
|
|
256
|
+
taxonomy_target = taxonomy_target.float()
|
|
257
|
+
|
|
258
|
+
# 1. Continuous Risk Huber Loss (with optional asymmetric risk weighting)
|
|
259
|
+
if risk_pos_weight > 1.0:
|
|
260
|
+
risk_weight = torch.where(risk_target >= 0.5, risk_pos_weight, 1.0)
|
|
261
|
+
l_risk = torch.mean(risk_weight * F.huber_loss(risk_pred, risk_target, delta=delta, reduction="none"))
|
|
262
|
+
else:
|
|
263
|
+
l_risk = F.huber_loss(risk_pred, risk_target, delta=delta)
|
|
264
|
+
|
|
265
|
+
# 2. Multi-Label Taxonomy Loss
|
|
266
|
+
l_tax = F.binary_cross_entropy_with_logits(taxonomy_logits, taxonomy_target, pos_weight=pos_weight)
|
|
267
|
+
|
|
268
|
+
# 3. Heteroscedastic Uncertainty Loss
|
|
269
|
+
diff_sq = (risk_target - risk_pred) ** 2
|
|
270
|
+
l_unc = torch.mean(0.5 * torch.exp(-log_variance) * diff_sq + 0.5 * log_variance)
|
|
271
|
+
|
|
272
|
+
# Total combined loss
|
|
273
|
+
if homoscedastic_weights is not None:
|
|
274
|
+
s1, s2, s3 = homoscedastic_weights
|
|
275
|
+
t_s1 = s1 if isinstance(s1, torch.Tensor) else torch.tensor(float(s1), device=risk_pred.device)
|
|
276
|
+
t_s2 = s2 if isinstance(s2, torch.Tensor) else torch.tensor(float(s2), device=risk_pred.device)
|
|
277
|
+
t_s3 = s3 if isinstance(s3, torch.Tensor) else torch.tensor(float(s3), device=risk_pred.device)
|
|
278
|
+
l_total = (
|
|
279
|
+
0.5 / (t_s1 ** 2) * l_risk
|
|
280
|
+
+ 0.5 / (t_s2 ** 2) * l_tax
|
|
281
|
+
+ 0.5 / (t_s3 ** 2) * l_unc
|
|
282
|
+
+ torch.log(torch.abs(t_s1 * t_s2 * t_s3) + 1e-8)
|
|
283
|
+
)
|
|
284
|
+
else:
|
|
285
|
+
l_total = l_risk + l_tax + l_unc
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
"loss_total": l_total,
|
|
289
|
+
"loss_risk": l_risk,
|
|
290
|
+
"loss_taxonomy": l_tax,
|
|
291
|
+
"loss_uncertainty": l_unc,
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class ModernBERTWithMultiTaskHead(ModernBERTMultiTaskModel):
|
|
296
|
+
"""
|
|
297
|
+
End-to-end ModernBERT encoder coupled with 3 multi-task evaluation heads:
|
|
298
|
+
1. Continuous calibrated risk regression (0.0 to 1.0)
|
|
299
|
+
2. Multi-label risk taxonomy (5 classes)
|
|
300
|
+
3. Epistemic uncertainty estimation
|
|
301
|
+
Matches the weights stored in fine-tuned model.safetensors.
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
def __init__(self, encoder_name: str = "answerdotai/ModernBERT-base"):
|
|
305
|
+
super().__init__(hidden_size_or_encoder=encoder_name)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class LayaDecisionHead:
|
|
309
|
+
"""
|
|
310
|
+
Lean neural decision head interfacing with Laya ModernBERT (421M large or 164M base).
|
|
311
|
+
Provides sub-50ms local verification with calibrated risk scores.
|
|
312
|
+
Prioritizes ONNX Runtime (dynamic INT8 or FP32) with graceful fallback to PyTorch
|
|
313
|
+
or deterministic heuristic gate verdicts.
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
DEFAULT_HF_REPO: str = "wxsys/tyranid-bert"
|
|
317
|
+
|
|
318
|
+
def __init__(
|
|
319
|
+
self,
|
|
320
|
+
weights_path: Optional[Path] = None,
|
|
321
|
+
enabled: bool = False,
|
|
322
|
+
quantize_int8: Optional[bool] = None,
|
|
323
|
+
risk_threshold: Optional[float] = None,
|
|
324
|
+
temperature: Optional[float] = None,
|
|
325
|
+
prefer_onnx: bool = True,
|
|
326
|
+
):
|
|
327
|
+
self.enabled = enabled
|
|
328
|
+
self.prefer_onnx = prefer_onnx
|
|
329
|
+
self._risk_threshold_explicit = risk_threshold is not None
|
|
330
|
+
if risk_threshold is not None:
|
|
331
|
+
self.risk_threshold = float(risk_threshold)
|
|
332
|
+
elif "CODE_ORACLE_RISK_THRESHOLD" in os.environ:
|
|
333
|
+
try:
|
|
334
|
+
self.risk_threshold = float(os.environ["CODE_ORACLE_RISK_THRESHOLD"])
|
|
335
|
+
except ValueError:
|
|
336
|
+
self.risk_threshold = 0.50
|
|
337
|
+
else:
|
|
338
|
+
self.risk_threshold = 0.50
|
|
339
|
+
|
|
340
|
+
self._temperature_explicit = temperature is not None
|
|
341
|
+
if temperature is not None:
|
|
342
|
+
self.temperature = float(temperature)
|
|
343
|
+
elif "CODE_ORACLE_TEMPERATURE" in os.environ:
|
|
344
|
+
try:
|
|
345
|
+
self.temperature = float(os.environ["CODE_ORACLE_TEMPERATURE"])
|
|
346
|
+
except ValueError:
|
|
347
|
+
self.temperature = 1.0
|
|
348
|
+
else:
|
|
349
|
+
self.temperature = 1.0
|
|
350
|
+
|
|
351
|
+
self.quantize_int8 = (
|
|
352
|
+
quantize_int8
|
|
353
|
+
if quantize_int8 is not None
|
|
354
|
+
else os.environ.get("CODE_ORACLE_INT8", "1").lower() in ("1", "true", "yes")
|
|
355
|
+
)
|
|
356
|
+
self._explicit_weights_path = weights_path
|
|
357
|
+
self.weights_path = self._resolve_weights_path(weights_path) if enabled else None
|
|
358
|
+
self.agent = None
|
|
359
|
+
self.pytorch_multitask_model: Optional[Any] = None
|
|
360
|
+
self.onnx_session: Optional[Any] = None
|
|
361
|
+
self.onnx_model_path: Optional[Path] = None
|
|
362
|
+
self.tokenizer = None
|
|
363
|
+
self.multi_task_model: Optional[ModernBERTMultiTaskModel] = (
|
|
364
|
+
ModernBERTMultiTaskModel() if HAS_TORCH else None
|
|
365
|
+
)
|
|
366
|
+
self._engine_mode: str = "heuristic"
|
|
367
|
+
self._loaded = False
|
|
368
|
+
if self.enabled and self.weights_path and self.weights_path.exists():
|
|
369
|
+
self._try_load_model()
|
|
370
|
+
|
|
371
|
+
def _resolve_weights_path(self, explicit_path: Optional[Path]) -> Optional[Path]:
|
|
372
|
+
if explicit_path:
|
|
373
|
+
return Path(explicit_path).resolve()
|
|
374
|
+
|
|
375
|
+
# Check explicit environment variable
|
|
376
|
+
env_weights = os.environ.get("CODE_ORACLE_WEIGHTS")
|
|
377
|
+
if env_weights and Path(env_weights).exists():
|
|
378
|
+
return Path(env_weights).resolve()
|
|
379
|
+
|
|
380
|
+
# Candidate local paths
|
|
381
|
+
candidates = [
|
|
382
|
+
Path.cwd() / ".code_oracle" / "weights",
|
|
383
|
+
Path.home() / ".cache" / "code_oracle" / "weights",
|
|
384
|
+
Path(__file__).resolve().parent / "weights",
|
|
385
|
+
Path.cwd() / "weights_base",
|
|
386
|
+
Path(__file__).resolve().parent.parent.parent / "weights_base",
|
|
387
|
+
Path.cwd() / "weights",
|
|
388
|
+
Path("/content/code_oracle_laya_base_model"),
|
|
389
|
+
Path("/content/code_oracle_laya_model"),
|
|
390
|
+
]
|
|
391
|
+
target_files = ["model_int8.onnx", "model.onnx", "model.safetensors"]
|
|
392
|
+
for c in candidates:
|
|
393
|
+
if c.exists() and any((c / f).exists() for f in target_files):
|
|
394
|
+
return c.resolve()
|
|
395
|
+
|
|
396
|
+
# Attempt downloading from Hugging Face Hub if auto-download enabled
|
|
397
|
+
auto_download = os.environ.get("CODE_ORACLE_AUTO_DOWNLOAD", "1").lower() in ("1", "true", "yes")
|
|
398
|
+
if auto_download:
|
|
399
|
+
try:
|
|
400
|
+
from huggingface_hub import snapshot_download
|
|
401
|
+
|
|
402
|
+
cache_dir = Path.home() / ".cache" / "code_oracle" / "weights"
|
|
403
|
+
repo_id = os.environ.get("CODE_ORACLE_HF_REPO", self.DEFAULT_HF_REPO)
|
|
404
|
+
hf_token = os.environ.get("HF_TOKEN")
|
|
405
|
+
downloaded = snapshot_download(
|
|
406
|
+
repo_id=repo_id,
|
|
407
|
+
local_dir=str(cache_dir),
|
|
408
|
+
token=hf_token,
|
|
409
|
+
)
|
|
410
|
+
p = Path(downloaded)
|
|
411
|
+
if p.exists() and any((p / f).exists() for f in target_files):
|
|
412
|
+
return p.resolve()
|
|
413
|
+
except Exception as exc:
|
|
414
|
+
logger.debug("Failed to auto-download weights from Hugging Face: %s", exc)
|
|
415
|
+
|
|
416
|
+
return None
|
|
417
|
+
|
|
418
|
+
@staticmethod
|
|
419
|
+
def _tune_cpu_threads() -> int:
|
|
420
|
+
"""
|
|
421
|
+
Auto-tune thread settings for CPU inference.
|
|
422
|
+
Restricts threads to physical cores to avoid hyperthread cache contention.
|
|
423
|
+
"""
|
|
424
|
+
try:
|
|
425
|
+
if HAS_TORCH and torch is not None and torch.cuda.is_available():
|
|
426
|
+
return torch.get_num_threads()
|
|
427
|
+
|
|
428
|
+
physical_cores = None
|
|
429
|
+
if os.path.exists("/proc/cpuinfo"):
|
|
430
|
+
try:
|
|
431
|
+
with open("/proc/cpuinfo", "r", encoding="utf-8") as f:
|
|
432
|
+
cores = set()
|
|
433
|
+
phys_id = "0"
|
|
434
|
+
for line in f:
|
|
435
|
+
if line.startswith("physical id"):
|
|
436
|
+
phys_id = line.split(":")[1].strip()
|
|
437
|
+
elif line.startswith("core id"):
|
|
438
|
+
core_id = line.split(":")[1].strip()
|
|
439
|
+
cores.add(f"{phys_id}:{core_id}")
|
|
440
|
+
if cores:
|
|
441
|
+
physical_cores = len(cores)
|
|
442
|
+
except Exception:
|
|
443
|
+
pass
|
|
444
|
+
|
|
445
|
+
if not physical_cores:
|
|
446
|
+
total = os.cpu_count() or 1
|
|
447
|
+
physical_cores = max(1, total // 2 if total > 2 else total)
|
|
448
|
+
|
|
449
|
+
tuned_threads = max(1, min(physical_cores, 8))
|
|
450
|
+
if HAS_TORCH and torch is not None:
|
|
451
|
+
torch.set_num_threads(tuned_threads)
|
|
452
|
+
return tuned_threads
|
|
453
|
+
except Exception:
|
|
454
|
+
return 1
|
|
455
|
+
|
|
456
|
+
def _load_tokenizer(self) -> None:
|
|
457
|
+
"""Load tokenizer using transformers or standalone tokenizers library."""
|
|
458
|
+
# 1. Try AutoTokenizer from transformers
|
|
459
|
+
try:
|
|
460
|
+
from transformers import AutoTokenizer
|
|
461
|
+
self.tokenizer = AutoTokenizer.from_pretrained(str(self.weights_path))
|
|
462
|
+
return
|
|
463
|
+
except Exception as e_tf:
|
|
464
|
+
logger.debug(f"AutoTokenizer loading skipped: {e_tf}")
|
|
465
|
+
|
|
466
|
+
# 2. Try pure tokenizers library from tokenizer.json
|
|
467
|
+
if self.weights_path is not None:
|
|
468
|
+
tok_json = self.weights_path / "tokenizer.json"
|
|
469
|
+
if tok_json.exists():
|
|
470
|
+
try:
|
|
471
|
+
from tokenizers import Tokenizer
|
|
472
|
+
self.tokenizer = Tokenizer.from_file(str(tok_json))
|
|
473
|
+
return
|
|
474
|
+
except Exception as e_tk:
|
|
475
|
+
logger.debug(f"tokenizers.Tokenizer loading skipped: {e_tk}")
|
|
476
|
+
|
|
477
|
+
def _tokenize(self, text: str, max_length: int = 512) -> Tuple[Any, Any]:
|
|
478
|
+
"""Tokenize text into numpy input_ids and attention_mask."""
|
|
479
|
+
if self.tokenizer is None:
|
|
480
|
+
raise RuntimeError("Tokenizer not initialized")
|
|
481
|
+
|
|
482
|
+
if hasattr(self.tokenizer, "batch_encode_plus") or callable(self.tokenizer):
|
|
483
|
+
try:
|
|
484
|
+
tokens = self.tokenizer(
|
|
485
|
+
text,
|
|
486
|
+
return_tensors="np",
|
|
487
|
+
truncation=True,
|
|
488
|
+
max_length=max_length,
|
|
489
|
+
)
|
|
490
|
+
return tokens["input_ids"].astype(np.int64), tokens["attention_mask"].astype(np.int64)
|
|
491
|
+
except Exception:
|
|
492
|
+
pass
|
|
493
|
+
|
|
494
|
+
if hasattr(self.tokenizer, "encode"):
|
|
495
|
+
enc = self.tokenizer.encode(text)
|
|
496
|
+
ids = enc.ids[:max_length]
|
|
497
|
+
mask = enc.attention_mask[:max_length]
|
|
498
|
+
return np.array([ids], dtype=np.int64), np.array([mask], dtype=np.int64)
|
|
499
|
+
|
|
500
|
+
raise RuntimeError("Unsupported tokenizer instance")
|
|
501
|
+
|
|
502
|
+
def _try_load_model(self) -> None:
|
|
503
|
+
try:
|
|
504
|
+
tuned_threads = self._tune_cpu_threads()
|
|
505
|
+
|
|
506
|
+
# Read config.json if available to extract threshold and calibrated temperature
|
|
507
|
+
config_file = self.weights_path / "config.json"
|
|
508
|
+
if config_file.exists():
|
|
509
|
+
try:
|
|
510
|
+
with open(config_file, "r", encoding="utf-8") as f_cfg:
|
|
511
|
+
cfg_data = json.load(f_cfg)
|
|
512
|
+
if not self._risk_threshold_explicit and "CODE_ORACLE_RISK_THRESHOLD" not in os.environ:
|
|
513
|
+
if "default_decision_threshold" in cfg_data:
|
|
514
|
+
self.risk_threshold = float(cfg_data["default_decision_threshold"])
|
|
515
|
+
if not self._temperature_explicit and "CODE_ORACLE_TEMPERATURE" not in os.environ:
|
|
516
|
+
if "calibrated_temperature" in cfg_data:
|
|
517
|
+
self.temperature = float(cfg_data["calibrated_temperature"])
|
|
518
|
+
except Exception as e_cfg:
|
|
519
|
+
logger.debug(f"Could not load config.json: {e_cfg}")
|
|
520
|
+
|
|
521
|
+
env_engine = os.environ.get("CODE_ORACLE_ENGINE", "").lower().strip()
|
|
522
|
+
if env_engine == "heuristic":
|
|
523
|
+
self._loaded = False
|
|
524
|
+
self._engine_mode = "heuristic"
|
|
525
|
+
return
|
|
526
|
+
|
|
527
|
+
# 1. Prioritize ONNX Runtime inference
|
|
528
|
+
if self.prefer_onnx and HAS_ORT and env_engine != "pytorch":
|
|
529
|
+
if env_engine == "onnx_fp32":
|
|
530
|
+
candidate_onnx_files = [self.weights_path / "model.onnx"]
|
|
531
|
+
elif env_engine == "onnx_int8":
|
|
532
|
+
candidate_onnx_files = [self.weights_path / "model_int8.onnx"]
|
|
533
|
+
elif self.quantize_int8:
|
|
534
|
+
candidate_onnx_files = [
|
|
535
|
+
self.weights_path / "model_int8.onnx",
|
|
536
|
+
self.weights_path / "model.onnx",
|
|
537
|
+
]
|
|
538
|
+
else:
|
|
539
|
+
candidate_onnx_files = [
|
|
540
|
+
self.weights_path / "model.onnx",
|
|
541
|
+
self.weights_path / "model_int8.onnx",
|
|
542
|
+
]
|
|
543
|
+
|
|
544
|
+
for onnx_file in candidate_onnx_files:
|
|
545
|
+
if onnx_file.exists():
|
|
546
|
+
try:
|
|
547
|
+
logger.info(f"Loading ONNX decision session from {onnx_file}")
|
|
548
|
+
sess_opts = ort.SessionOptions()
|
|
549
|
+
sess_opts.intra_op_num_threads = tuned_threads
|
|
550
|
+
sess_opts.inter_op_num_threads = 1
|
|
551
|
+
sess_opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
|
|
552
|
+
self.onnx_session = ort.InferenceSession(
|
|
553
|
+
str(onnx_file),
|
|
554
|
+
sess_options=sess_opts,
|
|
555
|
+
providers=["CPUExecutionProvider"],
|
|
556
|
+
)
|
|
557
|
+
self.onnx_model_path = onnx_file
|
|
558
|
+
self._engine_mode = "onnx_int8" if "int8" in onnx_file.name else "onnx_fp32"
|
|
559
|
+
self._load_tokenizer()
|
|
560
|
+
self._loaded = True
|
|
561
|
+
logger.info(f"Successfully loaded ONNX decision head ({self._engine_mode})")
|
|
562
|
+
return
|
|
563
|
+
except Exception as e_onnx:
|
|
564
|
+
logger.warning(f"Failed to load ONNX model {onnx_file}: {e_onnx}. Trying fallback...")
|
|
565
|
+
|
|
566
|
+
# 2. Check for native Multi-Task PyTorch safetensors model (fallback)
|
|
567
|
+
safetensors_file = self.weights_path / "model.safetensors"
|
|
568
|
+
if HAS_TORCH and safetensors_file.exists() and env_engine not in ("onnx_int8", "onnx_fp32"):
|
|
569
|
+
try:
|
|
570
|
+
from safetensors.torch import load_file
|
|
571
|
+
logger.info(f"Checking native Multi-Task ModernBERT weights from {self.weights_path}")
|
|
572
|
+
sd = load_file(str(safetensors_file))
|
|
573
|
+
if any("risk_head" in k for k in sd.keys()):
|
|
574
|
+
mt_model = ModernBERTWithMultiTaskHead()
|
|
575
|
+
mt_model.load_state_dict(sd)
|
|
576
|
+
mt_model.eval()
|
|
577
|
+
self.pytorch_multitask_model = mt_model
|
|
578
|
+
self._load_tokenizer()
|
|
579
|
+
self._engine_mode = "pytorch"
|
|
580
|
+
self._loaded = True
|
|
581
|
+
logger.info("Successfully loaded native Multi-Task ModernBERT PyTorch model")
|
|
582
|
+
return
|
|
583
|
+
except Exception as e_native:
|
|
584
|
+
logger.debug(f"Native Multi-Task loading skipped: {e_native}")
|
|
585
|
+
|
|
586
|
+
# 3. Legacy laya.load loader (fallback)
|
|
587
|
+
if env_engine not in ("onnx_int8", "onnx_fp32"):
|
|
588
|
+
try:
|
|
589
|
+
import laya
|
|
590
|
+
logger.info(f"Loading fine-tuned Laya weights from {self.weights_path}")
|
|
591
|
+
with warnings.catch_warnings(), contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
|
|
592
|
+
warnings.simplefilter("ignore")
|
|
593
|
+
self.agent = laya.load(str(self.weights_path))
|
|
594
|
+
|
|
595
|
+
if self.quantize_int8 and self.agent and hasattr(self.agent, "model") and HAS_TORCH:
|
|
596
|
+
try:
|
|
597
|
+
with warnings.catch_warnings():
|
|
598
|
+
warnings.simplefilter("ignore")
|
|
599
|
+
self.agent.model.encoder = torch.ao.quantization.quantize_dynamic(
|
|
600
|
+
self.agent.model.encoder,
|
|
601
|
+
{torch.nn.Linear},
|
|
602
|
+
dtype=torch.qint8,
|
|
603
|
+
)
|
|
604
|
+
logger.info("Applied dynamic INT8 quantization to encoder linear layers")
|
|
605
|
+
except Exception as q_err:
|
|
606
|
+
logger.debug(f"Dynamic INT8 quantization skipped: {q_err}")
|
|
607
|
+
|
|
608
|
+
self._engine_mode = "pytorch"
|
|
609
|
+
self._loaded = True
|
|
610
|
+
return
|
|
611
|
+
except Exception as e_laya:
|
|
612
|
+
logger.debug(f"Legacy laya loading skipped: {e_laya}")
|
|
613
|
+
|
|
614
|
+
self._loaded = False
|
|
615
|
+
self._engine_mode = "heuristic"
|
|
616
|
+
except Exception as e:
|
|
617
|
+
logger.warning(f"Could not load decision model from {self.weights_path}: {e}")
|
|
618
|
+
self.agent = None
|
|
619
|
+
self.pytorch_multitask_model = None
|
|
620
|
+
self.onnx_session = None
|
|
621
|
+
self._loaded = False
|
|
622
|
+
self._engine_mode = "heuristic"
|
|
623
|
+
|
|
624
|
+
def enable_neural_head(self) -> bool:
|
|
625
|
+
"""Dynamically enable and load neural head if weights exist and not loaded."""
|
|
626
|
+
if not self._loaded:
|
|
627
|
+
self.enabled = True
|
|
628
|
+
if not self.weights_path:
|
|
629
|
+
self.weights_path = self._resolve_weights_path(getattr(self, "_explicit_weights_path", None))
|
|
630
|
+
if self.weights_path and self.weights_path.exists():
|
|
631
|
+
self._try_load_model()
|
|
632
|
+
return self.is_neural_enabled
|
|
633
|
+
|
|
634
|
+
@property
|
|
635
|
+
def is_neural_enabled(self) -> bool:
|
|
636
|
+
"""Returns True if fine-tuned neural weights (ONNX, PyTorch, or legacy) are loaded and active."""
|
|
637
|
+
return self._loaded and (
|
|
638
|
+
self.onnx_session is not None
|
|
639
|
+
or self.pytorch_multitask_model is not None
|
|
640
|
+
or self.agent is not None
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
@property
|
|
644
|
+
def engine_mode(self) -> str:
|
|
645
|
+
"""Active engine status: 'onnx_int8', 'onnx_fp32', 'pytorch', or 'heuristic'."""
|
|
646
|
+
if not self.is_neural_enabled:
|
|
647
|
+
return "heuristic"
|
|
648
|
+
return self._engine_mode
|
|
649
|
+
|
|
650
|
+
def predict(
|
|
651
|
+
self,
|
|
652
|
+
linearized_dsl: str,
|
|
653
|
+
symbolic_status: str,
|
|
654
|
+
symbolic_confidence: float,
|
|
655
|
+
has_violations: bool,
|
|
656
|
+
) -> Tuple[str, float, float]:
|
|
657
|
+
"""
|
|
658
|
+
Evaluate linearized DSL subgraph.
|
|
659
|
+
Returns: (verdict_status, confidence, risk_score)
|
|
660
|
+
Maintains backward compatibility with 3-tuple return format.
|
|
661
|
+
"""
|
|
662
|
+
res = self.predict_multi_task(
|
|
663
|
+
linearized_dsl=linearized_dsl,
|
|
664
|
+
symbolic_status=symbolic_status,
|
|
665
|
+
symbolic_confidence=symbolic_confidence,
|
|
666
|
+
has_violations=has_violations,
|
|
667
|
+
)
|
|
668
|
+
return res.status, res.confidence, res.risk_score
|
|
669
|
+
|
|
670
|
+
def predict_multi_task(
|
|
671
|
+
self,
|
|
672
|
+
linearized_dsl: str,
|
|
673
|
+
symbolic_status: str,
|
|
674
|
+
symbolic_confidence: float,
|
|
675
|
+
has_violations: bool,
|
|
676
|
+
violations: Optional[List[str]] = None,
|
|
677
|
+
cycles: Optional[List[List[str]]] = None,
|
|
678
|
+
taxonomy_threshold: float = 0.5,
|
|
679
|
+
risk_threshold: Optional[float] = None,
|
|
680
|
+
temperature: Optional[float] = None,
|
|
681
|
+
) -> EnhancedDecisionResult:
|
|
682
|
+
"""
|
|
683
|
+
Evaluate linearized DSL subgraph with Multi-Task Risk Taxonomy
|
|
684
|
+
and Heteroscedastic Epistemic Uncertainty.
|
|
685
|
+
"""
|
|
686
|
+
violations = violations or []
|
|
687
|
+
cycles = cycles or []
|
|
688
|
+
eff_risk_threshold = self.risk_threshold if risk_threshold is None else risk_threshold
|
|
689
|
+
eff_temperature = self.temperature if temperature is None else temperature
|
|
690
|
+
|
|
691
|
+
# Hard rule: If deterministic symbolic gate caught a definite violation (cycle or arity),
|
|
692
|
+
# symbolic gate has absolute veto power (REJECTED).
|
|
693
|
+
if has_violations or symbolic_status == "REJECTED":
|
|
694
|
+
has_cycle = bool(cycles or any("CIRCULAR_DEPENDENCY" in v or "cycle" in v.lower() for v in violations))
|
|
695
|
+
has_broken_api = any(
|
|
696
|
+
any(k in v for k in ["ARITY_MISMATCH", "BROKEN_REFERENCE", "requires at least", "unexpected keyword", "SYNTAX_ERROR"])
|
|
697
|
+
for v in violations
|
|
698
|
+
) or not has_cycle
|
|
699
|
+
has_sec = any("security" in v.lower() or "auth" in v.lower() for v in violations)
|
|
700
|
+
has_perf = any("perf" in v.lower() or "loop" in v.lower() for v in violations)
|
|
701
|
+
|
|
702
|
+
tax_scores = RiskTaxonomyScores(
|
|
703
|
+
breaking_public_api=0.95 if has_broken_api else 0.40,
|
|
704
|
+
security_surface=0.90 if has_sec else 0.05,
|
|
705
|
+
concurrency_hazard=0.95 if has_cycle else 0.05,
|
|
706
|
+
performance_regression=0.90 if has_perf else 0.05,
|
|
707
|
+
silent_logic_drift=0.85,
|
|
708
|
+
)
|
|
709
|
+
return EnhancedDecisionResult(
|
|
710
|
+
status="REJECTED",
|
|
711
|
+
confidence=1.0,
|
|
712
|
+
risk_score=0.95,
|
|
713
|
+
epistemic_uncertainty=0.01,
|
|
714
|
+
risk_taxonomy=tax_scores,
|
|
715
|
+
active_risk_categories=tax_scores.active_categories(threshold=taxonomy_threshold),
|
|
716
|
+
is_neural_calibrated=False,
|
|
717
|
+
engine_mode=self.engine_mode,
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
# If neural weights are available, run inference
|
|
721
|
+
if self.is_neural_enabled:
|
|
722
|
+
# 1. Prioritized ONNX Runtime inference
|
|
723
|
+
if self.onnx_session is not None and self.tokenizer is not None and HAS_NUMPY:
|
|
724
|
+
try:
|
|
725
|
+
input_ids_np, attention_mask_np = self._tokenize(linearized_dsl, max_length=512)
|
|
726
|
+
ort_inputs = {
|
|
727
|
+
"input_ids": input_ids_np,
|
|
728
|
+
"attention_mask": attention_mask_np,
|
|
729
|
+
}
|
|
730
|
+
output_names = [o.name for o in self.onnx_session.get_outputs()]
|
|
731
|
+
ort_outputs = self.onnx_session.run(output_names, ort_inputs)
|
|
732
|
+
out_map = dict(zip(output_names, ort_outputs))
|
|
733
|
+
|
|
734
|
+
raw_risk = float(out_map["risk_score"][0][0])
|
|
735
|
+
raw_logits = float(out_map["risk_logits"][0][0]) if "risk_logits" in out_map else None
|
|
736
|
+
|
|
737
|
+
if eff_temperature is not None and eff_temperature > 0 and abs(eff_temperature - 1.0) > 1e-4:
|
|
738
|
+
if raw_logits is not None:
|
|
739
|
+
scaled_risk = 1.0 / (1.0 + math.exp(-raw_logits / eff_temperature))
|
|
740
|
+
else:
|
|
741
|
+
eps = 1e-6
|
|
742
|
+
clamped_r = min(max(raw_risk, eps), 1.0 - eps)
|
|
743
|
+
z = math.log(clamped_r / (1.0 - clamped_r))
|
|
744
|
+
scaled_risk = 1.0 / (1.0 + math.exp(-z / eff_temperature))
|
|
745
|
+
pred_risk = round(scaled_risk, 4)
|
|
746
|
+
else:
|
|
747
|
+
pred_risk = round(raw_risk, 4)
|
|
748
|
+
|
|
749
|
+
tax_probs = [float(x) for x in out_map["taxonomy_probs"][0]]
|
|
750
|
+
pred_conf = round(float(out_map["confidence"][0][0]), 4)
|
|
751
|
+
log_var = float(out_map["log_variance"][0][0])
|
|
752
|
+
epistemic_uncertainty = round(float(math.sqrt(math.exp(log_var))), 4)
|
|
753
|
+
|
|
754
|
+
pred_status = "APPROVED" if pred_risk < eff_risk_threshold else "REJECTED"
|
|
755
|
+
|
|
756
|
+
tax_scores = RiskTaxonomyScores(
|
|
757
|
+
breaking_public_api=round(tax_probs[0], 4),
|
|
758
|
+
security_surface=round(tax_probs[1], 4),
|
|
759
|
+
concurrency_hazard=round(tax_probs[2], 4),
|
|
760
|
+
performance_regression=round(tax_probs[3], 4),
|
|
761
|
+
silent_logic_drift=round(tax_probs[4], 4),
|
|
762
|
+
)
|
|
763
|
+
return EnhancedDecisionResult(
|
|
764
|
+
status=pred_status,
|
|
765
|
+
confidence=pred_conf,
|
|
766
|
+
risk_score=pred_risk,
|
|
767
|
+
epistemic_uncertainty=epistemic_uncertainty,
|
|
768
|
+
risk_taxonomy=tax_scores,
|
|
769
|
+
active_risk_categories=tax_scores.active_categories(threshold=taxonomy_threshold),
|
|
770
|
+
is_neural_calibrated=True,
|
|
771
|
+
engine_mode=self.engine_mode,
|
|
772
|
+
)
|
|
773
|
+
except Exception as e_onnx_inf:
|
|
774
|
+
logger.warning(f"ONNX inference error: {e_onnx_inf}. Falling back to PyTorch / heuristics.")
|
|
775
|
+
|
|
776
|
+
# 2. Native Multi-Task PyTorch Model
|
|
777
|
+
if self.pytorch_multitask_model is not None and self.tokenizer is not None and HAS_TORCH:
|
|
778
|
+
try:
|
|
779
|
+
tokens = self.tokenizer(
|
|
780
|
+
linearized_dsl,
|
|
781
|
+
return_tensors="pt",
|
|
782
|
+
truncation=True,
|
|
783
|
+
max_length=512,
|
|
784
|
+
)
|
|
785
|
+
with torch.no_grad():
|
|
786
|
+
out = self.pytorch_multitask_model(tokens["input_ids"], tokens["attention_mask"])
|
|
787
|
+
|
|
788
|
+
raw_risk = out["risk_score"].item()
|
|
789
|
+
raw_logits = out.get("risk_logits")
|
|
790
|
+
|
|
791
|
+
if eff_temperature is not None and eff_temperature > 0 and abs(eff_temperature - 1.0) > 1e-4:
|
|
792
|
+
if raw_logits is not None:
|
|
793
|
+
scaled_risk = torch.sigmoid(raw_logits / eff_temperature).item()
|
|
794
|
+
else:
|
|
795
|
+
eps = 1e-6
|
|
796
|
+
clamped_r = min(max(raw_risk, eps), 1.0 - eps)
|
|
797
|
+
z = math.log(clamped_r / (1.0 - clamped_r))
|
|
798
|
+
scaled_risk = 1.0 / (1.0 + math.exp(-z / eff_temperature))
|
|
799
|
+
pred_risk = round(scaled_risk, 4)
|
|
800
|
+
else:
|
|
801
|
+
pred_risk = round(raw_risk, 4)
|
|
802
|
+
|
|
803
|
+
tax_probs = out["taxonomy_probs"].squeeze(0).tolist()
|
|
804
|
+
pred_conf = round(out["confidence"].item(), 4)
|
|
805
|
+
s = out["log_variance"]
|
|
806
|
+
epistemic_uncertainty = round(float(torch.exp(s).sqrt().item()), 4)
|
|
807
|
+
|
|
808
|
+
pred_status = "APPROVED" if pred_risk < eff_risk_threshold else "REJECTED"
|
|
809
|
+
|
|
810
|
+
tax_scores = RiskTaxonomyScores(
|
|
811
|
+
breaking_public_api=round(float(tax_probs[0]), 4),
|
|
812
|
+
security_surface=round(float(tax_probs[1]), 4),
|
|
813
|
+
concurrency_hazard=round(float(tax_probs[2]), 4),
|
|
814
|
+
performance_regression=round(float(tax_probs[3]), 4),
|
|
815
|
+
silent_logic_drift=round(float(tax_probs[4]), 4),
|
|
816
|
+
)
|
|
817
|
+
return EnhancedDecisionResult(
|
|
818
|
+
status=pred_status,
|
|
819
|
+
confidence=pred_conf,
|
|
820
|
+
risk_score=pred_risk,
|
|
821
|
+
epistemic_uncertainty=epistemic_uncertainty,
|
|
822
|
+
risk_taxonomy=tax_scores,
|
|
823
|
+
active_risk_categories=tax_scores.active_categories(threshold=taxonomy_threshold),
|
|
824
|
+
is_neural_calibrated=True,
|
|
825
|
+
engine_mode="pytorch",
|
|
826
|
+
)
|
|
827
|
+
except Exception as e_nt:
|
|
828
|
+
logger.warning(f"Native Multi-Task inference error: {e_nt}. Falling back.")
|
|
829
|
+
|
|
830
|
+
# 3. Legacy laya agent
|
|
831
|
+
elif self.agent is not None:
|
|
832
|
+
try:
|
|
833
|
+
with warnings.catch_warnings(), contextlib.redirect_stdout(io.StringIO()):
|
|
834
|
+
warnings.simplefilter("ignore")
|
|
835
|
+
res = self.agent.predict(linearized_dsl, VERIFICATION_QUESTIONS)
|
|
836
|
+
status_ans = res["answers"]["status"]
|
|
837
|
+
risk_ans = res["answers"]["risk"]
|
|
838
|
+
|
|
839
|
+
pred_status = status_ans["choice"]
|
|
840
|
+
pred_confidence = max(symbolic_confidence, float(status_ans["confidence"]))
|
|
841
|
+
pred_risk = float(risk_ans["score"]) / 4.0 # Normalize 0..4 to 0.0..1.0
|
|
842
|
+
if pred_risk >= eff_risk_threshold:
|
|
843
|
+
pred_status = "REJECTED"
|
|
844
|
+
|
|
845
|
+
epistemic_uncertainty = max(0.0001, round((1.0 - pred_confidence) ** 2, 4))
|
|
846
|
+
|
|
847
|
+
has_api_drift = any(k in linearized_dsl for k in ["PARAM", "SIG", "ARITY", "DELETED"])
|
|
848
|
+
has_cycle_signal = any(k in linearized_dsl for k in ["CYCLE", "MUTUAL", "SCC"])
|
|
849
|
+
has_sec_signal = any(k in linearized_dsl.lower() for k in ["secret", "auth", "token", "pwd", "taint"])
|
|
850
|
+
has_perf_signal = any(k in linearized_dsl for k in ["LOOP", "QUERY", "PERF"])
|
|
851
|
+
|
|
852
|
+
tax_scores = RiskTaxonomyScores(
|
|
853
|
+
breaking_public_api=round(min(0.99, max(0.02, pred_risk * 1.2 if has_api_drift else pred_risk * 0.4)), 4),
|
|
854
|
+
security_surface=round(min(0.99, max(0.01, 0.85 if has_sec_signal else pred_risk * 0.15)), 4),
|
|
855
|
+
concurrency_hazard=round(min(0.99, max(0.01, 0.90 if has_cycle_signal else pred_risk * 0.2)), 4),
|
|
856
|
+
performance_regression=round(min(0.99, max(0.01, 0.85 if has_perf_signal else pred_risk * 0.2)), 4),
|
|
857
|
+
silent_logic_drift=round(min(0.95, max(0.02, pred_risk * 0.7)), 4),
|
|
858
|
+
)
|
|
859
|
+
|
|
860
|
+
return EnhancedDecisionResult(
|
|
861
|
+
status=pred_status,
|
|
862
|
+
confidence=pred_confidence,
|
|
863
|
+
risk_score=pred_risk,
|
|
864
|
+
epistemic_uncertainty=epistemic_uncertainty,
|
|
865
|
+
risk_taxonomy=tax_scores,
|
|
866
|
+
active_risk_categories=tax_scores.active_categories(threshold=taxonomy_threshold),
|
|
867
|
+
is_neural_calibrated=True,
|
|
868
|
+
engine_mode="pytorch",
|
|
869
|
+
)
|
|
870
|
+
except Exception as e:
|
|
871
|
+
logger.warning(f"Laya neural inference error: {e}. Falling back to symbolic gate.")
|
|
872
|
+
|
|
873
|
+
# Fallback to deterministic symbolic gate result
|
|
874
|
+
if symbolic_status == "APPROVED":
|
|
875
|
+
risk = 0.05
|
|
876
|
+
tax_scores = RiskTaxonomyScores(
|
|
877
|
+
breaking_public_api=0.02,
|
|
878
|
+
security_surface=0.01,
|
|
879
|
+
concurrency_hazard=0.01,
|
|
880
|
+
performance_regression=0.01,
|
|
881
|
+
silent_logic_drift=0.02,
|
|
882
|
+
)
|
|
883
|
+
else:
|
|
884
|
+
risk = 0.95
|
|
885
|
+
tax_scores = RiskTaxonomyScores(
|
|
886
|
+
breaking_public_api=0.95,
|
|
887
|
+
security_surface=0.05,
|
|
888
|
+
concurrency_hazard=0.10,
|
|
889
|
+
performance_regression=0.05,
|
|
890
|
+
silent_logic_drift=0.85,
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
fallback_status = symbolic_status
|
|
894
|
+
if fallback_status == "APPROVED" and risk >= eff_risk_threshold:
|
|
895
|
+
fallback_status = "REJECTED"
|
|
896
|
+
|
|
897
|
+
return EnhancedDecisionResult(
|
|
898
|
+
status=fallback_status,
|
|
899
|
+
confidence=symbolic_confidence,
|
|
900
|
+
risk_score=risk,
|
|
901
|
+
epistemic_uncertainty=0.02 if symbolic_status == "APPROVED" else 0.01,
|
|
902
|
+
risk_taxonomy=tax_scores,
|
|
903
|
+
active_risk_categories=tax_scores.active_categories(threshold=taxonomy_threshold),
|
|
904
|
+
is_neural_calibrated=False,
|
|
905
|
+
engine_mode="heuristic",
|
|
906
|
+
)
|