FatimaDataScientist72 commited on
Commit
fce816f
·
verified ·
1 Parent(s): df36bc7

Upload structural_patterns.py

Browse files
Files changed (1) hide show
  1. structural_patterns.py +388 -0
structural_patterns.py ADDED
@@ -0,0 +1,388 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MACI — Structural Pattern Layer (Phase 4)
3
+ -------------------------------------------
4
+ Built in direct response to SRB (Shariyah Review Bureau) review, 2026-07-31.
5
+ Arabic coverage added 2026-08-07, ahead of SRB follow-up meeting.
6
+
7
+ WHY THIS EXISTS:
8
+ The core XLM-R classifier is a language-pattern signal layer — it flags
9
+ text that reads like a known violation category (Riba, Gharar, etc).
10
+ SRB's review showed it correctly catches overt language-level issues
11
+ but misses violations that are STRUCTURAL: they only become apparent
12
+ by recognizing a known prohibited contract *pattern*, not a single
13
+ alarming phrase.
14
+
15
+ This module is a separate, deterministic, rule-based layer that runs
16
+ ALONGSIDE the ML classifier — not instead of it. It targets five
17
+ specific structural patterns, each modeled directly on a real clause
18
+ SRB identified as missed:
19
+
20
+ 1. Bay' al-Inah / mandatory buy-back structure (Schedule 4)
21
+ 2. Disguised Riba via "late payment as income" (Clause 10.6)
22
+ 3. Unilateral post-execution price variation (Clause 10.7)
23
+ 4. Risk/ownership decoupling (absolute payment
24
+ obligation regardless of asset condition) (Clause 10.5)
25
+ 5. Total liability waiver regardless of defect (Clause 7.3)
26
+
27
+ LANGUAGE COVERAGE:
28
+ Each pattern now matches both English and Arabic phrasing. Arabic
29
+ regexes were built by translating the five real clauses, then
30
+ validated against reworded/paraphrased Arabic (not just the direct
31
+ translations) to reduce overfitting to exact wording.
32
+
33
+ HONESTY NOTE — READ BEFORE PRESENTING:
34
+ - This is pattern-matching on KNOWN structural red flags, not general
35
+ reasoning. It will NOT catch every possible structural violation.
36
+ - The Arabic patterns were built and tested by a non-native-Arabic-
37
+ speaking developer (with AI assistance) using standard Islamic
38
+ finance terminology. They are NOT yet reviewed by a native Arabic
39
+ speaker or Shari'a-trained Arabic legal reviewer. Treat Arabic
40
+ coverage as a good-faith first pass, not certified/validated
41
+ coverage, until a native/scholar review has happened.
42
+ - Arabic morphology (verb conjugation, attached prefixes/suffixes)
43
+ makes literal phrase matching more fragile than in English. Recall
44
+ on genuinely novel Arabic phrasing (beyond what's been tested here)
45
+ is unverified and likely lower than English recall.
46
+ - Present this as: "targeted structural pattern detection, validated
47
+ against real SRB findings in English, with a first-pass Arabic
48
+ extension tested against paraphrase variants — not yet
49
+ scholar-reviewed for Arabic."
50
+ """
51
+
52
+ import re
53
+ from dataclasses import dataclass, field
54
+ from typing import List
55
+
56
+
57
+ @dataclass
58
+ class StructuralFlag:
59
+ pattern_id: str
60
+ name: str
61
+ violation_type: str
62
+ aaoifi_standard: str
63
+ severity: str
64
+ matched_text: str
65
+ explanation: str
66
+
67
+
68
+ @dataclass
69
+ class StructuralPattern:
70
+ pattern_id: str
71
+ name: str
72
+ violation_type: str
73
+ aaoifi_standard: str
74
+ severity: str
75
+ explanation: str
76
+ # A match requires ALL regexes in `all_of` to hit within a proximity
77
+ # window (order-independent), which avoids false positives from any
78
+ # single generic phrase appearing anywhere in a long document.
79
+ all_of: List[str] = field(default_factory=list)
80
+ # If ANY of these regexes also matches within the same window, the
81
+ # match is suppressed. Used to rule out genuine exceptions/negations
82
+ # that would otherwise trip the all_of conditions (e.g. a clause that
83
+ # explicitly says a price CANNOT be changed, discussing that fact,
84
+ # will still contain the words "price"/"changed"/"discretion" nearby).
85
+ none_of: List[str] = field(default_factory=list)
86
+
87
+
88
+ STRUCTURAL_PATTERNS: List[StructuralPattern] = [
89
+
90
+ StructuralPattern(
91
+ pattern_id="SP-01",
92
+ name="Bay' al-Inah / Mandatory Buy-Back Structure",
93
+ violation_type="Structural — disguised loan via buy-back",
94
+ aaoifi_standard="AAOIFI Shari'a Standard No. 8 (Murabaha), §3/1/2 — prohibition on Bay' al-Inah",
95
+ severity="CRITICAL",
96
+ explanation=(
97
+ "Clause requires the buyer to resell the same goods back to the "
98
+ "seller (or its affiliate) at a pre-agreed price. This is the "
99
+ "classic Bay' al-Inah structure — economically equivalent to an "
100
+ "interest-bearing loan disguised as two sales, and is prohibited "
101
+ "under mainstream AAOIFI-referenced Shari'a standards regardless "
102
+ "of the language used to describe it as a genuine sale."
103
+ ),
104
+ all_of=[
105
+ r"(resell|sell\s*back|re-?purchase|buy[-\s]?back|"
106
+ r"يعيد\s*بيع|إعادة\s*بيع|بيع\s*نفس|يشتري\s*مرة\s*أخرى|إعادة\s*الشراء|"
107
+ r"(?:بيع|شراء).{0,15}مرة\s*أخرى|مرة\s*أخرى.{0,15}(?:بيع|شراء)|"
108
+ r"دوبارہ\s*فروخت|واپس\s*فروخت|دوبارہ.{0,30}فروخت|فروخت.{0,30}دوبارہ)",
109
+ r"((the\s+)?(same\s+|identical\s+)?(goods|commodit(?:y|ies)|asset)s?|"
110
+ r"البضاعة|السلعة|الأصل|سامان|اثاثہ|اثاثے)",
111
+ r"(to\s+the\s+seller|to\s+the\s+bank|to\s+the\s+financier|"
112
+ r"its\s+designated\s+affiliate|to\s+the\s+original|"
113
+ r"إلى\s*البائع|للبائع|إلى\s*البنك|للبنك|الجهة\s*التابعة|"
114
+ r"بيچنے\s*والے\s*کو|بيچنے\s*والے\s*یا|بینک\s*کو)",
115
+ r"((pre[-\s]?agreed|fixed\s+in\s+advance|predetermined|agreed[-\s]?upon)\s+price|"
116
+ r"price\s+(?:is\s+|shall\s+be\s+|was\s+)?"
117
+ r"(pre[-\s]?agreed|fixed\s+in\s+advance|predetermined|agreed[-\s]?upon)|"
118
+ r"سعر\s*متفق\s*عليه\s*مسبقاً|بسعر\s*محدد\s*مسبقاً|سعر\s*ثابت\s*مسبقاً|"
119
+ r"پہلے\s*سے\s*طے\s*شدہ\s*قیمت|طے\s*شدہ\s*قیمت|مقررہ\s*قیمت)",
120
+ ],
121
+ ),
122
+
123
+ StructuralPattern(
124
+ pattern_id="SP-02",
125
+ name="Disguised Riba via Late-Payment-as-Income",
126
+ violation_type="Structural — interest disguised as penalty/fee income",
127
+ 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",
128
+ severity="CRITICAL",
129
+ explanation=(
130
+ "Clause imposes a recurring percentage-based late payment charge "
131
+ "that explicitly becomes part of the seller's own income/profit. "
132
+ "Under AAOIFI SS3, a compliant late-payment clause can only "
133
+ "impose a charge that is donated to charity — retaining it as "
134
+ "seller income is functionally identical to compound interest "
135
+ "on a defaulted debt, i.e. disguised Riba."
136
+ ),
137
+ all_of=[
138
+ r"(late\s+payment|overdue|delay(?:ed)?\s+in\s+payment|"
139
+ r"تأخر|تأخير|تاخیر|تاخیری)",
140
+ r"(charge|fee|penalty|surcharge|رسوم|غرامة|چارج|جرمانہ)",
141
+ r"(\d+(\.\d+)?\s*%\s*per\s*(week|month|day|annum)|"
142
+ r"(?:نسبة\s*)?\d+(?:\.\d+)?\s*%\s*(?:أسبوعياً|شهرياً|يومياً|سنوياً)|"
143
+ r"\d+(?:\.\d+)?\s*%\s*(?:ہفتہ\s*وار|ماہانہ|یومیہ|سالانہ))",
144
+ r"((seller'?s?|bank'?s?|financier'?s?)\s+(income|profit|revenue)|"
145
+ r"(retain(?:ed)?|form\s+part\s+of).{0,25}(income|profit|revenue)|"
146
+ r"دخل\s*البائع|دخل\s*البنك|جزءاً\s*من\s*دخل|"
147
+ r"آمدنی\s*کا\s*حصہ|آمدنی)",
148
+ ],
149
+ ),
150
+
151
+ StructuralPattern(
152
+ pattern_id="SP-03",
153
+ name="Unilateral Post-Execution Price Variation",
154
+ violation_type="Structural — Gharar via undisclosed/variable price",
155
+ aaoifi_standard="AAOIFI Shari'a Standard No. 8 (Murabaha), §2/2 — Murabaha price must be fixed and disclosed at contract execution",
156
+ severity="HIGH",
157
+ explanation=(
158
+ "Clause allows the seller to adjust the Murabaha sale price "
159
+ "after the agreement has been executed, at its sole discretion. "
160
+ "A valid Murabaha requires the cost and profit margin to be "
161
+ "fixed and disclosed to the buyer at the point of sale — "
162
+ "allowing unilateral post-execution adjustment reintroduces "
163
+ "exactly the price uncertainty (Gharar) that Murabaha's "
164
+ "disclosure requirement exists to prevent."
165
+ ),
166
+ all_of=[
167
+ r"(adjust(?:ed)?|vary|varied|amend(?:ed)?|chang(?:ed|e)|revis(?:e|ed)|"
168
+ r"تعديل|يعدل|تغيير|يغير|تبدیل)",
169
+ r"((sole|unfettered|absolute|unilateral)\s+discretion|"
170
+ r"تقديره\s*المطلق|تقديره\s*المنفرد|وفقاً\s*لتقديره|"
171
+ r"مطلق\s*صوابدید|اپنی\s*صوابدید)",
172
+ r"(price|murabaha\s+sale\s+price|sale\s+price|السعر|سعر\s*بيع\s*المرابحة|"
173
+ r"قیمت|مرابحہ\s*(?:فروخت\s*کی\s*)?قیمت)",
174
+ ],
175
+ none_of=[
176
+ r"cannot\s+be\s+chang(?:ed)?",
177
+ r"(shall|may|will)\s+not\s+be\s+chang(?:ed)?",
178
+ r"discretion\s+may\s+or\s+may\s+not\s+give\s+a\s+rebate",
179
+ r"\brebate\b",
180
+ r"لا\s*يجوز\s*تعديله",
181
+ r"\bخصم\s*تعجيل\b|\bخصم\s*مبكر\b", # early-settlement rebate carve-out
182
+ r"تبدیل\s*نہیں\s*کیا\s*جا\s*سکے\s*گا",
183
+ ],
184
+ ),
185
+
186
+ StructuralPattern(
187
+ pattern_id="SP-04",
188
+ name="Risk/Ownership Decoupling with Absolute Payment Obligation",
189
+ violation_type="Structural — Gharar/unjust risk allocation without ownership",
190
+ 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",
191
+ severity="HIGH",
192
+ explanation=(
193
+ "Clause shifts all risk (damage, loss, defects) to the buyer "
194
+ "from before/at the point of purchase while explicitly stating "
195
+ "the seller bears no risk, and separately makes the buyer's "
196
+ "payment obligation absolute and unconditional. A core Murabaha "
197
+ "requirement is that the seller carries genuine ownership risk "
198
+ "for some period before resale — structuring the deal so the "
199
+ "'seller' bears zero risk while the buyer's payment is "
200
+ "unconditional resembles a financing arrangement dressed as a "
201
+ "sale, undermining the risk-sharing basis that makes Murabaha "
202
+ "distinct from an interest-bearing loan."
203
+ ),
204
+ all_of=[
205
+ r"((seller|bank|financier)\s+shall\s+bear\s+(no|none\s+of\s+the)\s+risk|"
206
+ r"(bank|seller|financier)\s+shall\s+bear\s+none|"
207
+ r"لا\s*يتحمل\s*(?:البائع|البنك|الممول|الشركة)\s*(?:أي(?:ة)?)?\s*مخاطر|"
208
+ r"خطرہ.{0,20}(?:برداشت\s*نہیں\s*کرے|نہیں\s*اٹھائے)\s*گا)",
209
+ r"(absolute\s+and\s+unconditional|unconditional.{0,15}absolute|"
210
+ r"under\s+all\s+circumstances|"
211
+ r"مطلق(?:اً)?\s*وغير\s*مشروط|غير\s*مشروط.{0,15}مطلق|"
212
+ r"مطلق\s*اور\s*غیر\s*مشروط)",
213
+ ],
214
+ ),
215
+
216
+ StructuralPattern(
217
+ pattern_id="SP-05",
218
+ name="Total Liability Waiver Regardless of Defect/Title",
219
+ violation_type="Structural — waiver of Khiyar al-'Ayb (defect option) and title warranty",
220
+ aaoifi_standard="AAOIFI Shari'a Standard No. 8 (Murabaha), §4/6 — buyer retains recourse for defects the seller knew or should have known about",
221
+ severity="MEDIUM",
222
+ explanation=(
223
+ "Clause has the buyer waive all remedy against the seller for "
224
+ "defects in quality, condition, quantity, description, or even "
225
+ "title — deemed accepted 'unconditionally and without "
226
+ "reservation.' A total waiver of defect recourse, especially "
227
+ "including title defects, goes beyond normal 'as-is' sale terms "
228
+ "and can void the buyer's Shari'a-recognized right to remedy "
229
+ "for undisclosed defects (Khiyar al-'Ayb)."
230
+ ),
231
+ all_of=[
232
+ r"(no\s+(further\s+)?remedy|لا\s*يحق\s*له\s*أي\s*(?:مطالبة|تعويض)\s*آخر|"
233
+ r"لن\s*يكون\s*له\s*أي\s*حق\s*آخر|"
234
+ r"کوئی\s*(?:مزید\s*)?چارہ\s*جوئی|کوئی\s*مزید\s*دعو[یٰ])",
235
+ r"(unconditionally\s+and\s+without\s+reservation|"
236
+ r"غير\s*مشروط\s*ودون\s*(?:أي\s*)?تحفظ|بشكل\s*غير\s*مشروط.{0,15}تحفظ|"
237
+ r"غیر\s*مشروط\s*اور\s*بلا\s*کسی\s*تحفظ)",
238
+ ],
239
+ ),
240
+ ]
241
+
242
+
243
+ def normalize_arabic_script(s: str) -> str:
244
+ """
245
+ Normalizes Perso-Arabic script variants that look nearly identical
246
+ but are different Unicode codepoints, so a regex written with one
247
+ variant still matches text written with the other. This matters
248
+ because Arabic text conventionally uses U+064A/U+0643 while
249
+ Urdu/Farsi text conventionally uses U+06CC/U+06A9 for the same
250
+ visual letters — mixing them (even accidentally, e.g. when a
251
+ pattern is typed by hand) causes silent match failures that are
252
+ very hard to spot by eye, since the characters render identically.
253
+ """
254
+ s = s.replace("\u064a", "\u06cc") # Arabic yeh -> Urdu/Farsi yeh
255
+ s = s.replace("\u0649", "\u06cc") # Arabic alef maksura -> yeh
256
+ s = s.replace("\u0643", "\u06a9") # Arabic kaf -> Urdu/Farsi keheh
257
+ return s
258
+
259
+
260
+ def scan_structural_patterns(text: str) -> List[StructuralFlag]:
261
+ """
262
+ Runs all structural patterns against the given text.
263
+
264
+ Uses WINDOWED proximity matching: all component regexes for a pattern
265
+ must match within a bounded character window of each other (not just
266
+ anywhere in the whole input), to avoid false positives when scanning
267
+ long documents where unrelated clauses can each contribute one
268
+ matching phrase. Window size is tuned to comfortably span one clause
269
+ (~2-4 sentences) without spanning into unrelated adjacent clauses.
270
+ """
271
+ WINDOW = 350 # chars — roughly one clause's worth of context
272
+
273
+ flags = []
274
+ normalized = normalize_arabic_script(re.sub(r"\s+", " ", text))
275
+
276
+ for pattern in STRUCTURAL_PATTERNS:
277
+ # Find all match positions for each component regex. Regexes are
278
+ # also script-normalized so a pattern typed with one Arabic/Urdu
279
+ # letter variant still matches text using the other variant.
280
+ match_lists = []
281
+ ok = True
282
+ for rx in pattern.all_of:
283
+ rx_norm = normalize_arabic_script(rx)
284
+ matches = list(re.finditer(rx_norm, normalized, re.IGNORECASE))
285
+ if not matches:
286
+ ok = False
287
+ break
288
+ match_lists.append(matches)
289
+ if not ok:
290
+ continue
291
+
292
+ # Check whether there exists a combination of one match per regex
293
+ # such that all matches fall within WINDOW chars of each other.
294
+ # Anchor on the first regex's matches and check the others nearby.
295
+ found_cluster = None
296
+ for anchor in match_lists[0]:
297
+ center = (anchor.start() + anchor.end()) // 2
298
+ lo, hi = center - WINDOW, center + WINDOW
299
+ others_ok = True
300
+ cluster_positions = [anchor]
301
+ for other_matches in match_lists[1:]:
302
+ nearby = [m for m in other_matches if lo <= m.start() <= hi]
303
+ if not nearby:
304
+ others_ok = False
305
+ break
306
+ cluster_positions.append(nearby[0])
307
+ if others_ok:
308
+ found_cluster = cluster_positions
309
+ break
310
+
311
+ if found_cluster is None:
312
+ continue
313
+
314
+ # Build snippet around the cluster
315
+ starts = [m.start() for m in found_cluster]
316
+ ends = [m.end() for m in found_cluster]
317
+ snip_start = max(0, min(starts) - 40)
318
+ snip_end = min(len(normalized), max(ends) + 60)
319
+ snippet = normalized[snip_start:snip_end].strip()
320
+
321
+ # Exclusion check: if any none_of regex matches within the same
322
+ # window, this is a known exception/negation — suppress the flag.
323
+ window_text = normalized[max(0, snip_start - 100):min(len(normalized), snip_end + 100)]
324
+ if pattern.none_of and any(
325
+ re.search(normalize_arabic_script(rx), window_text, re.IGNORECASE)
326
+ for rx in pattern.none_of
327
+ ):
328
+ continue
329
+
330
+ flags.append(StructuralFlag(
331
+ pattern_id=pattern.pattern_id,
332
+ name=pattern.name,
333
+ violation_type=pattern.violation_type,
334
+ aaoifi_standard=pattern.aaoifi_standard,
335
+ severity=pattern.severity,
336
+ matched_text=f"...{snippet}...",
337
+ explanation=pattern.explanation,
338
+ ))
339
+
340
+ return flags
341
+
342
+
343
+ def structural_flags_to_dict(flags: List[StructuralFlag]) -> list:
344
+ return [
345
+ {
346
+ "pattern_id": f.pattern_id,
347
+ "name": f.name,
348
+ "violation_type": f.violation_type,
349
+ "aaoifi_standard": f.aaoifi_standard,
350
+ "severity": f.severity,
351
+ "matched_text": f.matched_text,
352
+ "explanation": f.explanation,
353
+ }
354
+ for f in flags
355
+ ]
356
+
357
+
358
+ if __name__ == "__main__":
359
+ import json
360
+ import sys
361
+ sys.path.insert(0, ".")
362
+ from test_clauses import (
363
+ CLAUSE_10_5, CLAUSE_10_6, CLAUSE_10_7, SCHEDULE_4, CLAUSE_7_3_DOC2,
364
+ CLAUSE_135092_COMPLIANT, CLAUSE_135096_COMPLIANT, CLAUSE_135093_COMPLIANT,
365
+ )
366
+
367
+ test_cases = [
368
+ ("10.5 — Risk/Ownership decoupling (SHOULD FLAG)", CLAUSE_10_5),
369
+ ("10.6 — Disguised Riba late payment (SHOULD FLAG)", CLAUSE_10_6),
370
+ ("10.7 — Unilateral price variation (SHOULD FLAG)", CLAUSE_10_7),
371
+ ("Schedule 4 — Bay' al-Inah buyback (SHOULD FLAG)", SCHEDULE_4),
372
+ ("Clause 7.3 — Total liability waiver (SHOULD FLAG)", CLAUSE_7_3_DOC2),
373
+ ("135092 — Actual-loss indemnity (SHOULD NOT FLAG)", CLAUSE_135092_COMPLIANT),
374
+ ("135096 — Actual-loss indemnity (SHOULD NOT FLAG)", CLAUSE_135096_COMPLIANT),
375
+ ("135093 — Tax definition (SHOULD NOT FLAG)", CLAUSE_135093_COMPLIANT),
376
+ ]
377
+
378
+ print("=" * 80)
379
+ for label, text in test_cases:
380
+ flags = scan_structural_patterns(text)
381
+ print(f"\n{label}")
382
+ if flags:
383
+ for f in flags:
384
+ print(f" ✅ FLAGGED [{f.pattern_id}] {f.name} — {f.severity}")
385
+ print(f" Standard: {f.aaoifi_standard}")
386
+ else:
387
+ print(" — no structural pattern matched —")
388
+ print("\n" + "=" * 80)