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
    ]