Spaces:
Sleeping
Sleeping
Delete structural_patterns.py
Browse files- 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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|