RoyAalekh commited on
Commit
2d34efc
·
1 Parent(s): 8d2e8fa

Enforce readiness evidence and unknown ripeness handling

Browse files
Files changed (1) hide show
  1. 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. Default to RIPE if no bottlenecks detected
 
 
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. Check stage-based ripeness (ripe stages are substantive)
120
- if case.current_stage in cls.RIPE_STAGES:
 
 
 
 
 
121
  return RipenessStatus.RIPE
122
-
123
- # 5. Default to RIPE if no bottlenecks detected
124
- # NOTE: Scheduling gap enforcement (MIN_GAP_BETWEEN_HEARINGS) is handled
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 data to determine ripeness",
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