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

Delete structural_patterns.py

Browse files
Files changed (1) hide show
  1. structural_patterns.py +0 -320
structural_patterns.py DELETED
@@ -1,320 +0,0 @@
1
- """
2
- MACI — Structural Pattern Layer (Phase 4)
3
- -------------------------------------------
4
- Built in direct response to SRB (Shariyah Review Bureau) review, 2026-07-31.
5
-
6
- WHY THIS EXISTS:
7
- The core XLM-R classifier is a language-pattern signal layer — it flags
8
- text that reads like a known violation category (Riba, Gharar, etc).
9
- SRB's review showed it correctly catches overt language-level issues
10
- but misses violations that are STRUCTURAL: they only become apparent
11
- by recognizing a known prohibited contract *pattern*, not a single
12
- alarming phrase.
13
-
14
- This module is a separate, deterministic, rule-based layer that runs
15
- ALONGSIDE the ML classifier — not instead of it. It targets four
16
- specific structural patterns, each modeled directly on a real clause
17
- SRB identified as missed:
18
-
19
- 1. Bay' al-Inah / mandatory buy-back structure (Schedule 4)
20
- 2. Disguised Riba via "late payment as income" (Clause 10.6)
21
- 3. Unilateral post-execution price variation (Clause 10.7)
22
- 4. Risk/ownership decoupling (absolute payment
23
- obligation regardless of asset condition) (Clause 10.5)
24
-
25
- HONESTY NOTE FOR PRESENTATION:
26
- This is pattern-matching on KNOWN structural red flags, not general
27
- reasoning. It will catch clauses that resemble these four patterns.
28
- It will NOT catch every possible structural violation — that remains
29
- future work, ideally built with SRB's domain input. Do not present
30
- this as "structural verdict" — present it as "targeted structural
31
- pattern detection, expanding from four validated real-world cases."
32
- """
33
-
34
- import re
35
- from dataclasses import dataclass, field
36
- from typing import List
37
-
38
-
39
- @dataclass
40
- class StructuralFlag:
41
- pattern_id: str
42
- name: str
43
- violation_type: str
44
- aaoifi_standard: str
45
- severity: str
46
- matched_text: str
47
- explanation: str
48
-
49
-
50
- @dataclass
51
- class StructuralPattern:
52
- pattern_id: str
53
- name: str
54
- violation_type: str
55
- aaoifi_standard: str
56
- severity: str
57
- explanation: str
58
- # A match requires ALL regexes in `all_of` to hit within a proximity
59
- # window (order-independent), which avoids false positives from any
60
- # single generic phrase appearing anywhere in a long document.
61
- all_of: List[str] = field(default_factory=list)
62
- # If ANY of these regexes also matches within the same window, the
63
- # match is suppressed. Used to rule out genuine exceptions/negations
64
- # that would otherwise trip the all_of conditions (e.g. a clause that
65
- # explicitly says a price CANNOT be changed, discussing that fact,
66
- # will still contain the words "price"/"changed"/"discretion" nearby).
67
- none_of: List[str] = field(default_factory=list)
68
-
69
-
70
- STRUCTURAL_PATTERNS: List[StructuralPattern] = [
71
-
72
- StructuralPattern(
73
- pattern_id="SP-01",
74
- name="Bay' al-Inah / Mandatory Buy-Back Structure",
75
- violation_type="Structural — disguised loan via buy-back",
76
- aaoifi_standard="AAOIFI Shari'a Standard No. 8 (Murabaha), §3/1/2 — prohibition on Bay' al-Inah",
77
- severity="CRITICAL",
78
- explanation=(
79
- "Clause requires the buyer to resell the same goods back to the "
80
- "seller (or its affiliate) at a pre-agreed price. This is the "
81
- "classic Bay' al-Inah structure — economically equivalent to an "
82
- "interest-bearing loan disguised as two sales, and is prohibited "
83
- "under mainstream AAOIFI-referenced Shari'a standards regardless "
84
- "of the language used to describe it as a genuine sale."
85
- ),
86
- all_of=[
87
- r"(resell|sell\s*back|re-?purchase|buy[-\s]?back)",
88
- r"(the\s+)?(same\s+|identical\s+)?(goods|commodit(?:y|ies)|asset)s?",
89
- r"(to\s+the\s+seller|to\s+the\s+bank|to\s+the\s+financier|its\s+designated\s+affiliate|to\s+the\s+original)",
90
- r"(pre[-\s]?agreed|fixed\s+in\s+advance|predetermined|agreed[-\s]?upon)\s+price|"
91
- r"price\s+(?:is\s+|shall\s+be\s+|was\s+)?"
92
- r"(pre[-\s]?agreed|fixed\s+in\s+advance|predetermined|agreed[-\s]?upon)",
93
- ],
94
- ),
95
-
96
- StructuralPattern(
97
- pattern_id="SP-02",
98
- name="Disguised Riba via Late-Payment-as-Income",
99
- violation_type="Structural — interest disguised as penalty/fee income",
100
- 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",
101
- severity="CRITICAL",
102
- explanation=(
103
- "Clause imposes a recurring percentage-based late payment charge "
104
- "that explicitly becomes part of the seller's own income/profit. "
105
- "Under AAOIFI SS3, a compliant late-payment clause can only "
106
- "impose a charge that is donated to charity — retaining it as "
107
- "seller income is functionally identical to compound interest "
108
- "on a defaulted debt, i.e. disguised Riba."
109
- ),
110
- all_of=[
111
- r"(late\s+payment|overdue|delay(?:ed)?\s+in\s+payment)",
112
- r"(charge|fee|penalty|surcharge)",
113
- r"\d+(\.\d+)?\s*%\s*per\s*(week|month|day|annum)",
114
- r"(seller'?s?|bank'?s?|financier'?s?)\s+(income|profit|revenue)|"
115
- r"(retain(?:ed)?|form\s+part\s+of).{0,25}(income|profit|revenue)",
116
- ],
117
- ),
118
-
119
- StructuralPattern(
120
- pattern_id="SP-03",
121
- name="Unilateral Post-Execution Price Variation",
122
- violation_type="Structural — Gharar via undisclosed/variable price",
123
- aaoifi_standard="AAOIFI Shari'a Standard No. 8 (Murabaha), §2/2 — Murabaha price must be fixed and disclosed at contract execution",
124
- severity="HIGH",
125
- explanation=(
126
- "Clause allows the seller to adjust the Murabaha sale price "
127
- "after the agreement has been executed, at its sole discretion. "
128
- "A valid Murabaha requires the cost and profit margin to be "
129
- "fixed and disclosed to the buyer at the point of sale — "
130
- "allowing unilateral post-execution adjustment reintroduces "
131
- "exactly the price uncertainty (Gharar) that Murabaha's "
132
- "disclosure requirement exists to prevent."
133
- ),
134
- all_of=[
135
- r"(adjust(?:ed)?|vary|varied|amend(?:ed)?|chang(?:ed|e)|revis(?:e|ed))",
136
- r"(sole|unfettered|absolute|unilateral)\s+discretion",
137
- r"(price|murabaha\s+sale\s+price|sale\s+price)",
138
- ],
139
- none_of=[
140
- r"cannot\s+be\s+chang(?:ed)?",
141
- r"(shall|may|will)\s+not\s+be\s+chang(?:ed)?",
142
- r"discretion\s+may\s+or\s+may\s+not\s+give\s+a\s+rebate",
143
- r"\brebate\b",
144
- ],
145
- ),
146
-
147
- StructuralPattern(
148
- pattern_id="SP-04",
149
- name="Risk/Ownership Decoupling with Absolute Payment Obligation",
150
- violation_type="Structural — Gharar/unjust risk allocation without ownership",
151
- 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",
152
- severity="HIGH",
153
- explanation=(
154
- "Clause shifts all risk (damage, loss, defects) to the buyer "
155
- "from before/at the point of purchase while explicitly stating "
156
- "the seller bears no risk, and separately makes the buyer's "
157
- "payment obligation absolute and unconditional. A core Murabaha "
158
- "requirement is that the seller carries genuine ownership risk "
159
- "for some period before resale — structuring the deal so the "
160
- "'seller' bears zero risk while the buyer's payment is "
161
- "unconditional resembles a financing arrangement dressed as a "
162
- "sale, undermining the risk-sharing basis that makes Murabaha "
163
- "distinct from an interest-bearing loan."
164
- ),
165
- all_of=[
166
- r"(seller|bank|financier)\s+shall\s+bear\s+(no|none\s+of\s+the)\s+risk|"
167
- r"(bank|seller|financier)\s+shall\s+bear\s+none",
168
- r"(absolute\s+and\s+unconditional|unconditional.{0,15}absolute|"
169
- r"under\s+all\s+circumstances)",
170
- ],
171
- ),
172
-
173
- StructuralPattern(
174
- pattern_id="SP-05",
175
- name="Total Liability Waiver Regardless of Defect/Title",
176
- violation_type="Structural — waiver of Khiyar al-'Ayb (defect option) and title warranty",
177
- aaoifi_standard="AAOIFI Shari'a Standard No. 8 (Murabaha), §4/6 — buyer retains recourse for defects the seller knew or should have known about",
178
- severity="MEDIUM",
179
- explanation=(
180
- "Clause has the buyer waive all remedy against the seller for "
181
- "defects in quality, condition, quantity, description, or even "
182
- "title — deemed accepted 'unconditionally and without "
183
- "reservation.' A total waiver of defect recourse, especially "
184
- "including title defects, goes beyond normal 'as-is' sale terms "
185
- "and can void the buyer's Shari'a-recognized right to remedy "
186
- "for undisclosed defects (Khiyar al-'Ayb)."
187
- ),
188
- all_of=[
189
- r"no\s+(further\s+)?remedy",
190
- r"unconditionally\s+and\s+without\s+reservation",
191
- ],
192
- ),
193
- ]
194
-
195
-
196
- def scan_structural_patterns(text: str) -> List[StructuralFlag]:
197
- """
198
- Runs all structural patterns against the given text.
199
-
200
- Uses WINDOWED proximity matching: all component regexes for a pattern
201
- must match within a bounded character window of each other (not just
202
- anywhere in the whole input), to avoid false positives when scanning
203
- long documents where unrelated clauses can each contribute one
204
- matching phrase. Window size is tuned to comfortably span one clause
205
- (~2-4 sentences) without spanning into unrelated adjacent clauses.
206
- """
207
- WINDOW = 350 # chars — roughly one clause's worth of context
208
-
209
- flags = []
210
- normalized = re.sub(r"\s+", " ", text)
211
-
212
- for pattern in STRUCTURAL_PATTERNS:
213
- # Find all match positions for each component regex
214
- match_lists = []
215
- ok = True
216
- for rx in pattern.all_of:
217
- matches = list(re.finditer(rx, normalized, re.IGNORECASE))
218
- if not matches:
219
- ok = False
220
- break
221
- match_lists.append(matches)
222
- if not ok:
223
- continue
224
-
225
- # Check whether there exists a combination of one match per regex
226
- # such that all matches fall within WINDOW chars of each other.
227
- # Anchor on the first regex's matches and check the others nearby.
228
- found_cluster = None
229
- for anchor in match_lists[0]:
230
- center = (anchor.start() + anchor.end()) // 2
231
- lo, hi = center - WINDOW, center + WINDOW
232
- others_ok = True
233
- cluster_positions = [anchor]
234
- for other_matches in match_lists[1:]:
235
- nearby = [m for m in other_matches if lo <= m.start() <= hi]
236
- if not nearby:
237
- others_ok = False
238
- break
239
- cluster_positions.append(nearby[0])
240
- if others_ok:
241
- found_cluster = cluster_positions
242
- break
243
-
244
- if found_cluster is None:
245
- continue
246
-
247
- # Build snippet around the cluster
248
- starts = [m.start() for m in found_cluster]
249
- ends = [m.end() for m in found_cluster]
250
- snip_start = max(0, min(starts) - 40)
251
- snip_end = min(len(normalized), max(ends) + 60)
252
- snippet = normalized[snip_start:snip_end].strip()
253
-
254
- # Exclusion check: if any none_of regex matches within the same
255
- # window, this is a known exception/negation — suppress the flag.
256
- window_text = normalized[max(0, snip_start - 100):min(len(normalized), snip_end + 100)]
257
- if pattern.none_of and any(
258
- re.search(rx, window_text, re.IGNORECASE) for rx in pattern.none_of
259
- ):
260
- continue
261
-
262
- flags.append(StructuralFlag(
263
- pattern_id=pattern.pattern_id,
264
- name=pattern.name,
265
- violation_type=pattern.violation_type,
266
- aaoifi_standard=pattern.aaoifi_standard,
267
- severity=pattern.severity,
268
- matched_text=f"...{snippet}...",
269
- explanation=pattern.explanation,
270
- ))
271
-
272
- return flags
273
-
274
-
275
- def structural_flags_to_dict(flags: List[StructuralFlag]) -> list:
276
- return [
277
- {
278
- "pattern_id": f.pattern_id,
279
- "name": f.name,
280
- "violation_type": f.violation_type,
281
- "aaoifi_standard": f.aaoifi_standard,
282
- "severity": f.severity,
283
- "matched_text": f.matched_text,
284
- "explanation": f.explanation,
285
- }
286
- for f in flags
287
- ]
288
-
289
-
290
- if __name__ == "__main__":
291
- import json
292
- import sys
293
- sys.path.insert(0, ".")
294
- from test_clauses import (
295
- CLAUSE_10_5, CLAUSE_10_6, CLAUSE_10_7, SCHEDULE_4, CLAUSE_7_3_DOC2,
296
- CLAUSE_135092_COMPLIANT, CLAUSE_135096_COMPLIANT, CLAUSE_135093_COMPLIANT,
297
- )
298
-
299
- test_cases = [
300
- ("10.5 — Risk/Ownership decoupling (SHOULD FLAG)", CLAUSE_10_5),
301
- ("10.6 — Disguised Riba late payment (SHOULD FLAG)", CLAUSE_10_6),
302
- ("10.7 — Unilateral price variation (SHOULD FLAG)", CLAUSE_10_7),
303
- ("Schedule 4 — Bay' al-Inah buyback (SHOULD FLAG)", SCHEDULE_4),
304
- ("Clause 7.3 — Total liability waiver (SHOULD FLAG)", CLAUSE_7_3_DOC2),
305
- ("135092 — Actual-loss indemnity (SHOULD NOT FLAG)", CLAUSE_135092_COMPLIANT),
306
- ("135096 — Actual-loss indemnity (SHOULD NOT FLAG)", CLAUSE_135096_COMPLIANT),
307
- ("135093 — Tax definition (SHOULD NOT FLAG)", CLAUSE_135093_COMPLIANT),
308
- ]
309
-
310
- print("=" * 80)
311
- for label, text in test_cases:
312
- flags = scan_structural_patterns(text)
313
- print(f"\n{label}")
314
- if flags:
315
- for f in flags:
316
- print(f" ✅ FLAGGED [{f.pattern_id}] {f.name} — {f.severity}")
317
- print(f" Standard: {f.aaoifi_standard}")
318
- else:
319
- print(" — no structural pattern matched —")
320
- print("\n" + "=" * 80)