Spaces:
Running
Running
File size: 13,313 Bytes
fce816f e922e90 fce816f e922e90 fce816f e922e90 fce816f e922e90 fce816f e922e90 | 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 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 | """
MACI — Structural Pattern Layer (Phase 4)
-------------------------------------------
Built in direct response to SRB (Shariyah Review Bureau) review, 2026-07-31.
Arabic coverage added 2026-08-07, ahead of SRB follow-up meeting.
"""
import re
from dataclasses import dataclass, field
from typing import List
@dataclass
class StructuralFlag:
pattern_id: str
name: str
violation_type: str
aaoifi_standard: str
severity: str
matched_text: str
explanation: str
@dataclass
class StructuralPattern:
pattern_id: str
name: str
violation_type: str
aaoifi_standard: str
severity: str
explanation: str
all_of: List[str] = field(default_factory=list)
none_of: List[str] = field(default_factory=list)
STRUCTURAL_PATTERNS: List[StructuralPattern] = [
StructuralPattern(
pattern_id="SP-01",
name="Bay' al-Inah / Mandatory Buy-Back Structure",
violation_type="Structural — disguised loan via buy-back",
aaoifi_standard="AAOIFI Shari'a Standard No. 8 (Murabaha), §3/1/2 — prohibition on Bay' al-Inah",
severity="CRITICAL",
explanation=(
"Clause requires the buyer to resell the same goods back to the "
"seller (or its affiliate) at a pre-agreed price. This is the "
"classic Bay' al-Inah structure — economically equivalent to an "
"interest-bearing loan disguised as two sales, and is prohibited "
"under mainstream AAOIFI-referenced Shari'a standards regardless "
"of the language used to describe it as a genuine sale."
),
all_of=[
r"(resell|sell\s*back|re-?purchase|buy[-\s]?back|"
r"يعيد\s*بيع|إعادة\s*بيع|بيع\s*نفس|يشتري\s*مرة\s*أخرى|إعادة\s*الشراء|"
r"(?:بيع|شراء).{0,15}مرة\s*أخرى|مرة\s*أخرى.{0,15}(?:بيع|شراء)|"
r"دوبارہ\s*فروخت|واپس\s*فروخت|دوبارہ.{0,30}فروخت|فروخت.{0,30}دوبارہ)",
r"((the\s+)?(same\s+|identical\s+)?(goods|commodit(?:y|ies)|asset)s?|"
r"البضاعة|السلعة|الأصل|سامان|اثاثہ|اثاثے)",
r"(to\s+the\s+seller|to\s+the\s+bank|to\s+the\s+financier|"
r"its\s+designated\s+affiliate|to\s+the\s+original|"
r"إلى\s*البائع|للبائع|إلى\s*البنك|للبنك|الجهة\s*التابعة|"
r"بيچنے\s*والے\s*کو|بيچنے\s*والے\s*یا|بینک\s*کو)",
r"((pre[-\s]?agreed|fixed\s+in\s+advance|predetermined|agreed[-\s]?upon)\s+price|"
r"price\s+(?:is\s+|shall\s+be\s+|was\s+)?"
r"(pre[-\s]?agreed|fixed\s+in\s+advance|predetermined|agreed[-\s]?upon)|"
r"سعر\s*متفق\s*عليه\s*مسبقاً|بسعر\s*محدد\s*مسبقاً|سعر\s*ثابت\s*مسبقاً|"
r"پہلے\s*سے\s*طے\s*شدہ\s*قیمت|طے\s*شدہ\s*قیمت|مقررہ\s*قیمت)",
],
),
StructuralPattern(
pattern_id="SP-02",
name="Disguised Riba via Late-Payment-as-Income",
violation_type="Structural — interest disguised as penalty/fee income",
aaoifi_standard="AAOIFI Shari'a Standard No. 3 (Default in Payment by a Debtor), §2 — late payment charges must go to charity, not seller income",
severity="CRITICAL",
explanation=(
"Clause imposes a recurring percentage-based late payment charge "
"that explicitly becomes part of the seller's own income/profit. "
"Under AAOIFI SS3, a compliant late-payment clause can only "
"impose a charge that is donated to charity — retaining it as "
"seller income is functionally identical to compound interest "
"on a defaulted debt, i.e. disguised Riba."
),
all_of=[
r"(late\s+payment|overdue|delay(?:ed)?\s+in\s+payment|"
r"تأخر|تأخير|تاخیر|تاخیری)",
r"(charge|fee|penalty|surcharge|رسوم|غرامة|چارج|جرمانہ)",
r"(\d+(\.\d+)?\s*%\s*per\s*(week|month|day|annum)|"
r"(?:نسبة\s*)?\d+(?:\.\d+)?\s*%\s*(?:أسبوعياً|شهرياً|يومياً|سنوياً)|"
r"\d+(?:\.\d+)?\s*%\s*(?:ہفتہ\s*وار|ماہانہ|یومیہ|سالانہ))",
r"((seller'?s?|bank'?s?|financier'?s?)\s+(income|profit|revenue)|"
r"(retain(?:ed)?|form\s+part\s+of).{0,25}(income|profit|revenue)|"
r"دخل\s*البائع|دخل\s*البنك|جزءاً\s*من\s*دخل|"
r"آمدنی\s*کا\s*حصہ|آمدنی)",
],
),
StructuralPattern(
pattern_id="SP-03",
name="Unilateral Post-Execution Price Variation",
violation_type="Structural — Gharar via undisclosed/variable price",
aaoifi_standard="AAOIFI Shari'a Standard No. 8 (Murabaha), §2/2 — Murabaha price must be fixed and disclosed at contract execution",
severity="HIGH",
explanation=(
"Clause allows the seller to adjust the Murabaha sale price "
"after the agreement has been executed, at its sole discretion. "
"A valid Murabaha requires the cost and profit margin to be "
"fixed and disclosed to the buyer at the point of sale — "
"allowing unilateral post-execution adjustment reintroduces "
"exactly the price uncertainty (Gharar) that Murabaha's "
"disclosure requirement exists to prevent."
),
all_of=[
r"(adjust(?:ed)?|vary|varied|amend(?:ed)?|chang(?:ed|e)|revis(?:e|ed)|"
r"تعديل|يعدل|تغيير|يغير|تبدیل)",
r"((sole|unfettered|absolute|unilateral)\s+discretion|"
r"تقديره\s*المطلق|تقديره\s*المنفرد|وفقاً\s*لتقديره|"
r"مطلق\s*صوابدید|اپنی\s*صوابدید)",
r"(price|murabaha\s+sale\s+price|sale\s+price|السعر|سعر\s*بيع\s*المرابحة|"
r"قیمت|مرابحہ\s*(?:فروخت\s*کی\s*)?قیمت)",
],
none_of=[
r"cannot\s+be\s+chang(?:ed)?",
r"(shall|may|will)\s+not\s+be\s+chang(?:ed)?",
r"discretion\s+may\s+or\s+may\s+not\s+give\s+a\s+rebate",
r"\brebate\b",
r"لا\s*يجوز\s*تعديله",
r"\bخصم\s*تعجيل\b|\bخصم\s*مبكر\b",
r"تبدیل\s*نہیں\s*کیا\s*جا\s*سکے\s*گا",
],
),
StructuralPattern(
pattern_id="SP-04",
name="Risk/Ownership Decoupling with Absolute Payment Obligation",
violation_type="Structural — Gharar/unjust risk allocation without ownership",
aaoifi_standard="AAOIFI Shari'a Standard No. 8 (Murabaha), §5/1 — seller must bear ownership risk before/at point of sale; buyer's payment obligation cannot be wholly absolute regardless of asset condition",
severity="HIGH",
explanation=(
"Clause shifts all risk (damage, loss, defects) to the buyer "
"from before/at the point of purchase while explicitly stating "
"the seller bears no risk, and separately makes the buyer's "
"payment obligation absolute and unconditional. A core Murabaha "
"requirement is that the seller carries genuine ownership risk "
"for some period before resale — structuring the deal so the "
"'seller' bears zero risk while the buyer's payment is "
"unconditional resembles a financing arrangement dressed as a "
"sale, undermining the risk-sharing basis that makes Murabaha "
"distinct from an interest-bearing loan."
),
all_of=[
r"((seller|bank|financier)\s+shall\s+bear\s+(no|none\s+of\s+the)\s+risk|"
r"(bank|seller|financier)\s+shall\s+bear\s+none|"
r"لا\s*يتحمل\s*(?:البائع|البنك|الممول|الشركة)\s*(?:أي(?:ة)?)?\s*مخاطر|"
r"خطرہ.{0,20}(?:برداشت\s*نہیں\s*کرے|نہیں\s*اٹھائے)\s*گا)",
r"(absolute\s+and\s+unconditional|unconditional.{0,15}absolute|"
r"under\s+all\s+circumstances|"
r"مطلق(?:اً)?\s*وغير\s*مشروط|غير\s*مشروط.{0,15}مطلق|"
r"مطلق\s*اور\s*غیر\s*مشروط)",
],
),
StructuralPattern(
pattern_id="SP-05",
name="Total Liability Waiver Regardless of Defect/Title",
violation_type="Structural — waiver of Khiyar al-'Ayb (defect option) and title warranty",
aaoifi_standard="AAOIFI Shari'a Standard No. 8 (Murabaha), §4/6 — buyer retains recourse for defects the seller knew or should have known about",
severity="MEDIUM",
explanation=(
"Clause has the buyer waive all remedy against the seller for "
"defects in quality, condition, quantity, description, or even "
"title — deemed accepted 'unconditionally and without "
"reservation.' A total waiver of defect recourse, especially "
"including title defects, goes beyond normal 'as-is' sale terms "
"and can void the buyer's Shari'a-recognized right to remedy "
"for undisclosed defects (Khiyar al-'Ayb)."
),
all_of=[
r"(no\s+(further\s+)?remedy|لا\s*يحق\s*له\s*أي\s*(?:مطالبة|تعويض)\s*آخر|"
r"لن\s*يكون\s*له\s*أي\s*حق\s*آخر|"
r"کوئی\s*(?:مزید\s*)?چارہ\s*جوئی|کوئی\s*مزید\s*دعو[یٰ])",
r"(unconditionally\s+and\s+without\s+reservation|"
r"غير\s*مشروط\s*ودون\s*(?:أي\s*)?تحفظ|بشكل\s*غير\s*مشروط.{0,15}تحفظ|"
r"غیر\s*مشروط\s*اور\s*بلا\s*کسی\s*تحفظ)",
],
),
]
def normalize_arabic_script(s: str) -> str:
"""
Normalizes Perso-Arabic script variants that look nearly identical
but are different Unicode codepoints, so a regex written with one
variant still matches text written with the other.
"""
s = s.replace("\u064a", "\u06cc") # Arabic yeh -> Urdu/Farsi yeh
s = s.replace("\u0649", "\u06cc") # Arabic alef maksura -> yeh
s = s.replace("\u0643", "\u06a9") # Arabic kaf -> Urdu/Farsi keheh
return s
def scan_structural_patterns(text: str) -> List[StructuralFlag]:
"""
Runs all structural patterns against the given text using windowed
proximity matching (all component regexes must match within ~one
clause's worth of characters of each other).
"""
WINDOW = 350
flags = []
normalized = normalize_arabic_script(re.sub(r"\s+", " ", text))
for pattern in STRUCTURAL_PATTERNS:
match_lists = []
ok = True
for rx in pattern.all_of:
rx_norm = normalize_arabic_script(rx)
matches = list(re.finditer(rx_norm, normalized, re.IGNORECASE))
if not matches:
ok = False
break
match_lists.append(matches)
if not ok:
continue
found_cluster = None
for anchor in match_lists[0]:
center = (anchor.start() + anchor.end()) // 2
lo, hi = center - WINDOW, center + WINDOW
others_ok = True
cluster_positions = [anchor]
for other_matches in match_lists[1:]:
nearby = [m for m in other_matches if lo <= m.start() <= hi]
if not nearby:
others_ok = False
break
cluster_positions.append(nearby[0])
if others_ok:
found_cluster = cluster_positions
break
if found_cluster is None:
continue
starts = [m.start() for m in found_cluster]
ends = [m.end() for m in found_cluster]
snip_start = max(0, min(starts) - 40)
snip_end = min(len(normalized), max(ends) + 60)
snippet = normalized[snip_start:snip_end].strip()
window_text = normalized[max(0, snip_start - 100):min(len(normalized), snip_end + 100)]
if pattern.none_of and any(
re.search(normalize_arabic_script(rx), window_text, re.IGNORECASE)
for rx in pattern.none_of
):
continue
flags.append(StructuralFlag(
pattern_id=pattern.pattern_id,
name=pattern.name,
violation_type=pattern.violation_type,
aaoifi_standard=pattern.aaoifi_standard,
severity=pattern.severity,
matched_text=f"...{snippet}...",
explanation=pattern.explanation,
))
return flags
def structural_flags_to_dict(flags: List[StructuralFlag]) -> list:
return [
{
"pattern_id": f.pattern_id,
"name": f.name,
"violation_type": f.violation_type,
"aaoifi_standard": f.aaoifi_standard,
"severity": f.severity,
"matched_text": f.matched_text,
"explanation": f.explanation,
}
for f in flags
] |