Spaces:
Running
Running
| """ | |
| 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 | |
| class StructuralFlag: | |
| pattern_id: str | |
| name: str | |
| violation_type: str | |
| aaoifi_standard: str | |
| severity: str | |
| matched_text: str | |
| explanation: str | |
| 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 | |
| ] |