Spaces:
Sleeping
Sleeping
File size: 11,043 Bytes
f989668 0d94d59 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 | """
MACI — Reconciliation Layer (Phase 5)
--------------------------------------
Closes two gaps identified in pre-SRB-meeting adversarial testing
(2026-08-10), against the live production API:
GAP 1 — Recall: the base XLM-R classifier missed two real structural
violations (Bay' al-Inah buy-back, risk/ownership decoupling) when
presented in Arabic/Urdu, despite catching the English-phrased
equivalents. The structural_patterns.py layer (Phase 4) already
detects these correctly in all three languages — it just wasn't
being used to CHANGE the final verdict, only to add supplementary
flags alongside whatever the ML model said.
GAP 2 — Precision: the base classifier false-positives on standard
AAOIFI-compliant actual-loss indemnity language (Murabaha default
compensation calculated as cost-price-vs-resale-price difference),
flagging it as Gharar/HIGH. This is the exact clause shape AAOIFI
Shari'a Standard No. 8 §7 explicitly permits.
WHAT THIS MODULE DOES:
Runs structural_patterns.scan_structural_patterns() and a new,
narrow set of COMPLIANT hard-negative patterns alongside the base
classifier, then reconciles the two signals into one verdict:
1. If a structural VIOLATION pattern fires (SP-01..SP-05), it wins,
regardless of what the base classifier said. These are
deterministic, SRB-derived patterns — higher trust than a
probabilistic ML flag missing on this language.
2. If the base classifier flags a violation, no structural violation
fired, AND a known compliant hard-negative pattern matches, the
verdict is DOWNGRADED — not silently cleared. It becomes
"REVIEW" with an explicit reconciliation note, never "AUTHENTIC"
outright. A human still looks at it; the difference is that
"definitely quarantine this" becomes "a human should confirm
this, here is exactly why it might be a false positive."
3. Otherwise, the base classifier's verdict passes through
unchanged.
WHAT THIS MODULE DELIBERATELY DOES NOT DO:
- It does not retrain or touch classifier weights. The false
positive on clause type #7 is still present in the base model —
this suppresses its effect on the final verdict for a known,
narrow pattern, it does not fix the model's understanding.
- It does not downgrade CRITICAL-severity ML flags. A pattern this
narrow is not trusted to override the base model's highest-
confidence violation category (Riba) even if surface phrasing
happens to overlap — only HIGH/MEDIUM Gharar-type flags, which is
where the observed false positive actually lives.
- The hard-negative list starts at ONE pattern (the exact one found
in testing), not a broad category. Expand it only against new,
specifically observed false positives — a wide net here just
trades a false-positive problem for a false-negative one.
HONESTY NOTE — same posture as structural_patterns.py:
This is a targeted patch for two specific, observed failure modes,
not a general precision/recall fix. It should be presented as
exactly that.
"""
import re
from dataclasses import dataclass, field
from typing import List, Optional
from structural_patterns import (
scan_structural_patterns,
normalize_arabic_script,
StructuralFlag,
)
@dataclass
class HardNegativePattern:
pattern_id: str
name: str
aaoifi_standard: str
explanation: str
all_of: List[str] = field(default_factory=list)
# If any ML-flagged violation_class is NOT in this set, the
# hard-negative is ignored — e.g. this pattern should only ever
# suppress a Gharar flag, never a Riba flag, even if it matched.
applies_to_violation_classes: List[str] = field(default_factory=list)
# Never downgrade at this severity or above.
max_ml_severity: str = "HIGH"
# Severity ordering used for the max_ml_severity guard.
_SEVERITY_RANK = {"NONE": 0, "LOW": 1, "MEDIUM": 2, "HIGH": 3, "CRITICAL": 4}
COMPLIANT_HARD_NEGATIVES: List[HardNegativePattern] = [
HardNegativePattern(
pattern_id="CHN-01",
name="Actual-Loss Indemnity (Cost-vs-Resale-Price Difference)",
aaoifi_standard=(
"AAOIFI Shari'a Standard No. 8 (Murabaha), §7 — compensation "
"for actual loss on a defaulted purchase undertaking, "
"calculated as a concrete formula rather than an open-ended "
"or discretionary penalty, is permitted and is not Gharar."
),
explanation=(
"Clause compensates the seller for the difference between the "
"cost price and the actual resale price obtained from a third "
"party after a failed purchase undertaking. This is a fixed, "
"calculable, after-the-fact formula tied to a real transaction "
"— not an open-ended or discretionary amount — which is the "
"specific feature that distinguishes permitted actual-loss "
"indemnity from prohibited Gharar-bearing penalty clauses. "
"This is standard, AAOIFI-compliant Murabaha default-"
"compensation language, not excessive uncertainty."
),
all_of=[
r"(compensate|indemnify|reimburse|pay.{0,20}(the\s+)?difference|"
r"يعوض|تعويض|يضمن)",
r"(difference\s+between|الفرق\s+بين|فرق\s*(?:بين)?)",
r"((cost\s+price|purchase\s+price|original\s+price)|"
r"سعر\s*(?:التكلفة|الشراء|الأصلي))",
r"((the\s+)?sale\s+price|(re)?sale\s*proceeds|"
r"سعر\s*(?:البيع|إعادة\s*البيع))",
r"(third\s+part(?:y|ies)|طرف\s*ثالث|أطراف\s*ثالثة)",
],
applies_to_violation_classes=["Gharar (Excessive Uncertainty)"],
max_ml_severity="HIGH",
),
]
def scan_hard_negatives(text: str, ml_violation_class: str, ml_severity: str) -> Optional[HardNegativePattern]:
"""
Returns the first matching hard-negative pattern applicable to this
ML result, or None. Applies the violation-class and max-severity
guards before even attempting the regex match, so this can never
be used to downgrade a violation class or severity it wasn't
explicitly built for.
"""
normalized = normalize_arabic_script(re.sub(r"\s+", " ", text))
ml_rank = _SEVERITY_RANK.get(ml_severity, 0)
for pattern in COMPLIANT_HARD_NEGATIVES:
if ml_violation_class not in pattern.applies_to_violation_classes:
continue
if ml_rank > _SEVERITY_RANK.get(pattern.max_ml_severity, 0):
continue
all_matched = True
for rx in pattern.all_of:
if not re.search(normalize_arabic_script(rx), normalized, re.IGNORECASE):
all_matched = False
break
if all_matched:
return pattern
return None
def reconcile(text: str, ml_result: dict) -> dict:
"""
Takes the raw classify() output dict (matching the shape in
main.py's classify() — expects ml_result["maci_evaluation"] with
violation_class / severity / result / confidence_band, and
ml_result["boundary_behavior"] with recommendation) and returns
a new dict with those fields reconciled against the structural
and hard-negative layers, plus a "reconciliation" block that
always explains what happened (even when nothing changed) so the
audit trail is honest about which layer produced the final call.
"""
ev = ml_result["maci_evaluation"]
boundary = ml_result["boundary_behavior"]
structural_flags: List[StructuralFlag] = scan_structural_patterns(text)
reconciliation = {
"applied": False,
"source": None,
"reason": None,
"original_ml_violation_class": ev["violation_class"],
"original_ml_severity": ev["severity"],
"original_ml_result": ev["result"],
"original_recommendation": boundary["recommendation"],
}
# ── Case 1: structural violation overrides, regardless of ML ──
if structural_flags:
top = max(
structural_flags,
key=lambda f: _SEVERITY_RANK.get(f.severity, 0),
)
ev["violation_class"] = top.name
ev["result"] = "FLAGGED"
ev["severity"] = top.severity
ev["confidence_band"] = "HIGH"
ev["model_used"] = ev.get("model_used", "") + " + structural_pattern_layer"
boundary["recommendation"] = (
"QUARANTINE" if top.severity == "CRITICAL" else "ESCALATE"
)
boundary["review_required"] = True
boundary["review_reason"] = f"structural_pattern_match:{top.pattern_id}"
ml_result["maqasid_enrichment"] = ml_result.get("maqasid_enrichment", {})
ml_result["maqasid_enrichment"]["aaoifi_standard"] = top.aaoifi_standard
ml_result["maqasid_enrichment"]["structural_fix"] = top.explanation
reconciliation.update({
"applied": True,
"source": "structural_override",
"reason": (
f"Deterministic structural pattern {top.pattern_id} "
f"({top.name}) matched — this pattern is trusted over "
f"the base classifier's verdict for this text, "
f"regardless of language."
),
"structural_matches": [f.pattern_id for f in structural_flags],
})
# ── Case 2: hard-negative downgrade (only if no structural hit) ──
else:
hn = scan_hard_negatives(text, ev["violation_class"], ev["severity"])
if hn is not None and ev["result"] not in ("AUTHENTIC",):
ev["result"] = "REVIEW"
boundary["recommendation"] = "REVIEW"
boundary["review_required"] = True
boundary["review_reason"] = f"hard_negative_pattern_match:{hn.pattern_id}"
ml_result["maqasid_enrichment"] = ml_result.get("maqasid_enrichment", {})
ml_result["maqasid_enrichment"]["aaoifi_standard"] = hn.aaoifi_standard
ml_result["maqasid_enrichment"]["compliant_alternative"] = (
"No change required — this clause matches a recognized "
"AAOIFI-compliant pattern. Flagged for human confirmation, "
"not auto-cleared."
)
reconciliation.update({
"applied": True,
"source": "hard_negative_downgrade",
"reason": (
f"Base classifier flagged '{ev['violation_class']}' "
f"({reconciliation['original_ml_severity']}), but this text "
f"matches known compliant pattern {hn.pattern_id} "
f"({hn.name}). Downgraded from an automatic "
f"{reconciliation['original_recommendation']} to REVIEW — "
f"a human confirms, the system does not self-clear."
),
})
ml_result["reconciliation"] = reconciliation
return ml_result |