Spaces:
Sleeping
Sleeping
Enforce readiness evidence and unknown ripeness handling
Browse files- scheduler/core/ripeness.py +66 -18
scheduler/core/ripeness.py
CHANGED
|
@@ -28,7 +28,7 @@ class RipenessStatus(Enum):
|
|
| 28 |
def is_ripe(self) -> bool:
|
| 29 |
"""Check if status indicates ripeness."""
|
| 30 |
return self == RipenessStatus.RIPE
|
| 31 |
-
|
| 32 |
def is_unripe(self) -> bool:
|
| 33 |
"""Check if status indicates unripeness."""
|
| 34 |
return self in {
|
|
@@ -54,11 +54,11 @@ RIPE_KEYWORDS = ["ARGUMENTS", "HEARING", "FINAL", "JUDGMENT", "ORDERS", "DISPOSA
|
|
| 54 |
|
| 55 |
class RipenessClassifier:
|
| 56 |
"""Classify cases as RIPE or UNRIPE for scheduling optimization."""
|
| 57 |
-
|
| 58 |
# Stages that indicate case is ready for substantive hearing
|
| 59 |
RIPE_STAGES = [
|
| 60 |
"ARGUMENTS",
|
| 61 |
-
"EVIDENCE",
|
| 62 |
"ORDERS / JUDGMENT",
|
| 63 |
"FINAL DISPOSAL"
|
| 64 |
]
|
|
@@ -70,6 +70,50 @@ class RipenessClassifier:
|
|
| 70 |
"FRAMING OF CHARGES",
|
| 71 |
"INTERLOCUTORY APPLICATION"
|
| 72 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
@classmethod
|
| 75 |
def classify(cls, case: Case, current_date: datetime | None = None) -> RipenessStatus:
|
|
@@ -81,13 +125,15 @@ class RipenessClassifier:
|
|
| 81 |
|
| 82 |
Returns:
|
| 83 |
RipenessStatus enum indicating ripeness and bottleneck type
|
| 84 |
-
|
| 85 |
Algorithm:
|
| 86 |
1. Check last hearing purpose for explicit bottleneck keywords
|
| 87 |
2. Check stage (ADMISSION vs ORDERS/JUDGMENT)
|
| 88 |
3. Check case maturity (days since filing, hearing count)
|
| 89 |
4. Check if stuck (many hearings but no progress)
|
| 90 |
-
5.
|
|
|
|
|
|
|
| 91 |
"""
|
| 92 |
if current_date is None:
|
| 93 |
current_date = datetime.now()
|
|
@@ -95,7 +141,7 @@ class RipenessClassifier:
|
|
| 95 |
# 1. Check last hearing purpose for explicit bottleneck keywords
|
| 96 |
if hasattr(case, "last_hearing_purpose") and case.last_hearing_purpose:
|
| 97 |
purpose_upper = case.last_hearing_purpose.upper()
|
| 98 |
-
|
| 99 |
for keyword, bottleneck_type in UNRIPE_KEYWORDS.items():
|
| 100 |
if keyword in purpose_upper:
|
| 101 |
return bottleneck_type
|
|
@@ -105,26 +151,28 @@ class RipenessClassifier:
|
|
| 105 |
# New cases in ADMISSION (< 3 hearings) are often unripe
|
| 106 |
if case.hearing_count < 3:
|
| 107 |
return RipenessStatus.UNRIPE_SUMMONS
|
| 108 |
-
|
| 109 |
# 3. Check if case is "stuck" (many hearings but no progress)
|
| 110 |
if case.hearing_count > 10:
|
| 111 |
# Calculate average days between hearings
|
| 112 |
if case.age_days > 0:
|
| 113 |
avg_gap = case.age_days / case.hearing_count
|
| 114 |
-
|
| 115 |
# If average gap > 60 days, likely stuck due to bottleneck
|
| 116 |
if avg_gap > 60:
|
| 117 |
return RipenessStatus.UNRIPE_PARTY
|
| 118 |
-
|
| 119 |
-
# 4.
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
return RipenessStatus.RIPE
|
| 122 |
-
|
| 123 |
-
#
|
| 124 |
-
|
| 125 |
-
# by the simulation engine, not the ripeness classifier. Ripeness only
|
| 126 |
-
# detects substantive bottlenecks (summons, dependencies, party issues).
|
| 127 |
-
return RipenessStatus.RIPE
|
| 128 |
|
| 129 |
@classmethod
|
| 130 |
def get_ripeness_priority(cls, case: Case, current_date: datetime | None = None) -> float:
|
|
@@ -183,7 +231,7 @@ class RipenessClassifier:
|
|
| 183 |
RipenessStatus.UNRIPE_DEPENDENT: "Waiting for another case or court order",
|
| 184 |
RipenessStatus.UNRIPE_PARTY: "Party or lawyer unavailable",
|
| 185 |
RipenessStatus.UNRIPE_DOCUMENT: "Missing documents or evidence",
|
| 186 |
-
RipenessStatus.UNKNOWN: "Insufficient
|
| 187 |
}
|
| 188 |
return reasons.get(ripeness_status, "Unknown status")
|
| 189 |
|
|
|
|
| 28 |
def is_ripe(self) -> bool:
|
| 29 |
"""Check if status indicates ripeness."""
|
| 30 |
return self == RipenessStatus.RIPE
|
| 31 |
+
|
| 32 |
def is_unripe(self) -> bool:
|
| 33 |
"""Check if status indicates unripeness."""
|
| 34 |
return self in {
|
|
|
|
| 54 |
|
| 55 |
class RipenessClassifier:
|
| 56 |
"""Classify cases as RIPE or UNRIPE for scheduling optimization."""
|
| 57 |
+
|
| 58 |
# Stages that indicate case is ready for substantive hearing
|
| 59 |
RIPE_STAGES = [
|
| 60 |
"ARGUMENTS",
|
| 61 |
+
"EVIDENCE",
|
| 62 |
"ORDERS / JUDGMENT",
|
| 63 |
"FINAL DISPOSAL"
|
| 64 |
]
|
|
|
|
| 70 |
"FRAMING OF CHARGES",
|
| 71 |
"INTERLOCUTORY APPLICATION"
|
| 72 |
]
|
| 73 |
+
|
| 74 |
+
# Minimum evidence thresholds before declaring a case RIPE
|
| 75 |
+
MIN_SERVICE_HEARINGS = 1 # At least one hearing to confirm service/compliance
|
| 76 |
+
MIN_STAGE_DAYS = 7 # Time spent in current stage to show compliance efforts
|
| 77 |
+
MIN_CASE_AGE_DAYS = 14 # Minimum maturity before assuming readiness
|
| 78 |
+
|
| 79 |
+
@classmethod
|
| 80 |
+
def _has_required_evidence(cls, case: Case) -> tuple[bool, dict[str, bool]]:
|
| 81 |
+
"""Check that minimum readiness evidence exists before declaring RIPE."""
|
| 82 |
+
|
| 83 |
+
# Evidence of service/compliance: at least one hearing or explicit purpose text
|
| 84 |
+
service_confirmed = case.hearing_count >= cls.MIN_SERVICE_HEARINGS or bool(
|
| 85 |
+
getattr(case, "last_hearing_purpose", None)
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
# Evidence the case has progressed in its current stage
|
| 89 |
+
days_in_stage = getattr(case, "days_in_stage", 0)
|
| 90 |
+
compliance_confirmed = (
|
| 91 |
+
case.current_stage not in cls.UNRIPE_STAGES or days_in_stage >= cls.MIN_STAGE_DAYS
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
# Age-based maturity requirement
|
| 95 |
+
age_confirmed = getattr(case, "age_days", 0) >= cls.MIN_CASE_AGE_DAYS
|
| 96 |
+
|
| 97 |
+
evidence = {
|
| 98 |
+
"service": service_confirmed,
|
| 99 |
+
"compliance": compliance_confirmed,
|
| 100 |
+
"age": age_confirmed,
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
return all(evidence.values()), evidence
|
| 104 |
+
|
| 105 |
+
@classmethod
|
| 106 |
+
def _has_ripe_signal(cls, case: Case) -> bool:
|
| 107 |
+
"""Check if stage or hearing purpose indicates readiness."""
|
| 108 |
+
|
| 109 |
+
if case.current_stage in cls.RIPE_STAGES:
|
| 110 |
+
return True
|
| 111 |
+
|
| 112 |
+
if hasattr(case, "last_hearing_purpose") and case.last_hearing_purpose:
|
| 113 |
+
purpose_upper = case.last_hearing_purpose.upper()
|
| 114 |
+
return any(keyword in purpose_upper for keyword in RIPE_KEYWORDS)
|
| 115 |
+
|
| 116 |
+
return False
|
| 117 |
|
| 118 |
@classmethod
|
| 119 |
def classify(cls, case: Case, current_date: datetime | None = None) -> RipenessStatus:
|
|
|
|
| 125 |
|
| 126 |
Returns:
|
| 127 |
RipenessStatus enum indicating ripeness and bottleneck type
|
| 128 |
+
|
| 129 |
Algorithm:
|
| 130 |
1. Check last hearing purpose for explicit bottleneck keywords
|
| 131 |
2. Check stage (ADMISSION vs ORDERS/JUDGMENT)
|
| 132 |
3. Check case maturity (days since filing, hearing count)
|
| 133 |
4. Check if stuck (many hearings but no progress)
|
| 134 |
+
5. Require readiness evidence (service/compliance/age) else UNKNOWN
|
| 135 |
+
6. Check explicit ripe signals (stage/purpose)
|
| 136 |
+
7. Default to UNKNOWN if evidence exists but no ripe signal
|
| 137 |
"""
|
| 138 |
if current_date is None:
|
| 139 |
current_date = datetime.now()
|
|
|
|
| 141 |
# 1. Check last hearing purpose for explicit bottleneck keywords
|
| 142 |
if hasattr(case, "last_hearing_purpose") and case.last_hearing_purpose:
|
| 143 |
purpose_upper = case.last_hearing_purpose.upper()
|
| 144 |
+
|
| 145 |
for keyword, bottleneck_type in UNRIPE_KEYWORDS.items():
|
| 146 |
if keyword in purpose_upper:
|
| 147 |
return bottleneck_type
|
|
|
|
| 151 |
# New cases in ADMISSION (< 3 hearings) are often unripe
|
| 152 |
if case.hearing_count < 3:
|
| 153 |
return RipenessStatus.UNRIPE_SUMMONS
|
| 154 |
+
|
| 155 |
# 3. Check if case is "stuck" (many hearings but no progress)
|
| 156 |
if case.hearing_count > 10:
|
| 157 |
# Calculate average days between hearings
|
| 158 |
if case.age_days > 0:
|
| 159 |
avg_gap = case.age_days / case.hearing_count
|
| 160 |
+
|
| 161 |
# If average gap > 60 days, likely stuck due to bottleneck
|
| 162 |
if avg_gap > 60:
|
| 163 |
return RipenessStatus.UNRIPE_PARTY
|
| 164 |
+
|
| 165 |
+
# 4. Require explicit readiness evidence before declaring RIPE
|
| 166 |
+
evidence_ok, _ = cls._has_required_evidence(case)
|
| 167 |
+
if not evidence_ok:
|
| 168 |
+
return RipenessStatus.UNKNOWN
|
| 169 |
+
|
| 170 |
+
# 5. Check stage-based ripeness (ripe stages are substantive) or explicit RIPE signal
|
| 171 |
+
if cls._has_ripe_signal(case):
|
| 172 |
return RipenessStatus.RIPE
|
| 173 |
+
|
| 174 |
+
# 6. Default to UNKNOWN if no bottlenecks but also no clear ripe signal
|
| 175 |
+
return RipenessStatus.UNKNOWN
|
|
|
|
|
|
|
|
|
|
| 176 |
|
| 177 |
@classmethod
|
| 178 |
def get_ripeness_priority(cls, case: Case, current_date: datetime | None = None) -> float:
|
|
|
|
| 231 |
RipenessStatus.UNRIPE_DEPENDENT: "Waiting for another case or court order",
|
| 232 |
RipenessStatus.UNRIPE_PARTY: "Party or lawyer unavailable",
|
| 233 |
RipenessStatus.UNRIPE_DOCUMENT: "Missing documents or evidence",
|
| 234 |
+
RipenessStatus.UNKNOWN: "Insufficient readiness evidence; route to manual triage",
|
| 235 |
}
|
| 236 |
return reasons.get(ripeness_status, "Unknown status")
|
| 237 |
|