paperhawk / domain_checks /check_09_dd_red_flags.py
Nándorfi Vince
Initial paperhawk push to HF Space (LFS for binaries)
7ff7119
raw
history blame
4.72 kB
"""09: DD red flags (M&A best practice) — A/B level, universal.
4 red flags:
1. Missing change-of-control clause for high-value contracts (MEDIUM)
— value > 4.83M parity watermark
2. Auto-renewal (MEDIUM) — unpredictable obligation
3. Non-compete clause (MEDIUM) — buyer flexibility constraint
4. Non-assignable contract (HIGH) — critical for M&A
"""
from __future__ import annotations
from domain_checks.base import make_risk
from domain_checks.check_08_gdpr_28 import _get_full_text, _text_contains_any
from graph.states.pipeline_state import Risk
from utils.numbers import coerce_number
_REGULATION = "M&A DD best practice"
_VALUE_THRESHOLD = 4_830_000 # parity watermark for ~5M
class DDRedFlagsCheck:
check_id = "check_09_dd_red_flags"
regulation = _REGULATION
is_hu_specific = False
applies_to = {"contract"}
def apply(self, extracted: dict) -> list[Risk]:
risks: list[Risk] = []
full_text = _get_full_text(extracted)
# 1. Missing change-of-control clause — value > threshold AND no mention
value_dict = extracted.get("value") or {}
if isinstance(value_dict, dict) and value_dict:
total = coerce_number(value_dict.get("amount"))
else:
total = coerce_number(extracted.get("total_value"))
has_coc = _text_contains_any(full_text, [
"change of control", "change-of-control", "ownership change",
"acquisition", "buyout",
"tulajdonosváltozás", "irányításváltozás", "változás az irányításban",
"kontrollváltozás", "felvasárl", "akvizíció",
"Kontrollwechsel", "Eigentümerwechsel",
])
if total is not None and total > _VALUE_THRESHOLD and not has_coc:
risks.append(make_risk(
description="Missing change-of-control clause in a high-value contract",
severity="medium",
rationale=(
f"Contract value is {total:,.0f}, but no change-of-control "
f"clause is present. In an acquisition, the contract's "
f"future would be uncertain."
),
regulation=_REGULATION,
source_check_id=self.check_id,
))
# 2. Auto-renewal
has_auto_renewal = _text_contains_any(full_text, [
"auto-renewal", "automatic renewal", "evergreen clause",
"automatically renewed",
"automatikusan megújul", "hallgatólagos megújítás", "meghosszabbodik",
"automatische Verlängerung",
])
if has_auto_renewal:
risks.append(make_risk(
description="Auto-renewal clause detected",
severity="medium",
rationale=(
"The contract contains an auto-renewal clause. From a DD "
"perspective, this creates an open-ended obligation."
),
regulation=_REGULATION,
source_check_id=self.check_id,
))
# 3. Non-compete / restrictive covenant
has_non_compete = _text_contains_any(full_text, [
"non-compete", "non compete", "restrictive covenant",
"may not engage in",
"versenytilalm", "versenykorlátozás", "versenytilalom", "nem folytathat",
"Wettbewerbsverbot",
])
if has_non_compete:
risks.append(make_risk(
description="Non-compete clause detected",
severity="medium",
rationale=(
"The contract contains a non-compete clause. In an M&A "
"context, EU practice limits these to a maximum of 2 years."
),
regulation=_REGULATION,
source_check_id=self.check_id,
))
# 4. Non-assignable contract
has_no_assignment = _text_contains_any(full_text, [
"not assignable", "assignment prohibited", "no assignment",
"may not be assigned",
"nem ruházható át", "nem engedményezhető", "átruházás tilalma",
"nicht übertragbar",
])
if has_no_assignment:
risks.append(make_risk(
description="Contract assignment restriction",
severity="high",
rationale=(
"The contract is non-assignable. After an acquisition, the "
"new owner cannot automatically step into the contract."
),
regulation=_REGULATION,
source_check_id=self.check_id,
))
return risks