feat(halacha-triage): quality-gated + prioritized review queue + metrics (#84) #93
@@ -3792,7 +3792,19 @@ async def list_halachot(
|
||||
practice_area: str | None = None,
|
||||
limit: int = 200,
|
||||
offset: int = 0,
|
||||
exclude_low_quality: bool = False,
|
||||
order_by_priority: bool = False,
|
||||
) -> list[dict]:
|
||||
"""List halachot with optional triage controls (#84).
|
||||
|
||||
exclude_low_quality — drop items carrying ANY quality_flag (application /
|
||||
truncated_quote / quote_unverified / non_decision / thin_restatement /
|
||||
nli_unsupported / near_duplicate). These belong in a 'needs extraction
|
||||
fix' bucket, not the chair's approve queue (#84.1).
|
||||
order_by_priority — replace FIFO with an active-learning order (#84.3):
|
||||
negatively-treated first, then most-uncertain (lowest confidence), then
|
||||
oldest — so the chair sees the highest-value decisions first.
|
||||
"""
|
||||
pool = await get_pool()
|
||||
conditions = []
|
||||
params: list = []
|
||||
@@ -3809,7 +3821,16 @@ async def list_halachot(
|
||||
conditions.append(f"${idx} = ANY(h.practice_areas)")
|
||||
params.append(practice_area)
|
||||
idx += 1
|
||||
if exclude_low_quality:
|
||||
# a clean item has an empty/NULL quality_flags array
|
||||
conditions.append("COALESCE(array_length(h.quality_flags, 1), 0) = 0")
|
||||
where_sql = f"WHERE {' AND '.join(conditions)}" if conditions else ""
|
||||
order_sql = (
|
||||
"ORDER BY corroboration_negative DESC, h.confidence ASC NULLS LAST, "
|
||||
"h.created_at ASC"
|
||||
if order_by_priority
|
||||
else "ORDER BY h.case_law_id, h.halacha_index"
|
||||
)
|
||||
params.extend([limit, offset])
|
||||
sql = f"""
|
||||
SELECT h.id, h.case_law_id, h.halacha_index, h.rule_statement,
|
||||
@@ -3837,7 +3858,7 @@ async def list_halachot(
|
||||
GROUP BY halacha_id
|
||||
) cor ON cor.halacha_id = h.id
|
||||
{where_sql}
|
||||
ORDER BY h.case_law_id, h.halacha_index
|
||||
{order_sql}
|
||||
LIMIT ${idx} OFFSET ${idx + 1}
|
||||
"""
|
||||
rows = await pool.fetch(sql, *params)
|
||||
|
||||
@@ -117,12 +117,33 @@ async def halacha_backlog(conn) -> dict:
|
||||
oldest = await conn.fetchval(
|
||||
"SELECT MIN(created_at) FROM halachot WHERE review_status = 'pending_review'"
|
||||
)
|
||||
# #84.7 — split the pending bucket: how many are genuine candidates (clean)
|
||||
# vs flagged 'needs extraction fix', and the breakdown by flag, so the chair
|
||||
# sees how much of the backlog is real review vs extraction noise.
|
||||
pending_clean = await conn.fetchval(
|
||||
"SELECT COUNT(*) FROM halachot WHERE review_status = 'pending_review' "
|
||||
"AND COALESCE(array_length(quality_flags, 1), 0) = 0"
|
||||
)
|
||||
flag_rows = await conn.fetch(
|
||||
"SELECT flag, COUNT(*) AS n FROM ("
|
||||
" SELECT unnest(quality_flags) AS flag FROM halachot "
|
||||
" WHERE review_status = 'pending_review'"
|
||||
") t GROUP BY flag ORDER BY n DESC"
|
||||
)
|
||||
pending_total = counts.get("pending_review", 0)
|
||||
reviewed = counts.get("approved", 0) + counts.get("rejected", 0) + counts.get("published", 0)
|
||||
return {
|
||||
"pending_review": counts.get("pending_review", 0),
|
||||
"pending_review": pending_total,
|
||||
"pending_clean": pending_clean, # real review candidates (#84.1)
|
||||
"pending_flagged": pending_total - pending_clean, # needs-fix bucket
|
||||
"approved": counts.get("approved", 0),
|
||||
"rejected": counts.get("rejected", 0),
|
||||
"deferred": counts.get("deferred", 0),
|
||||
"published": counts.get("published", 0),
|
||||
"total": sum(counts.values()),
|
||||
"reviewed_total": reviewed,
|
||||
"approve_ratio": round(counts.get("approved", 0) / reviewed, 3) if reviewed else None,
|
||||
"pending_by_flag": {r["flag"]: r["n"] for r in flag_rows},
|
||||
"oldest_pending_at": oldest.isoformat() if oldest else None,
|
||||
}
|
||||
|
||||
|
||||
@@ -356,7 +356,22 @@ async def halacha_review(
|
||||
return _ok(row)
|
||||
|
||||
|
||||
async def halachot_pending(limit: int = 100) -> str:
|
||||
"""תור ההלכות הממתינות לאישור (review_status='pending_review')."""
|
||||
rows = await db.list_halachot(review_status="pending_review", limit=limit)
|
||||
async def halachot_pending(limit: int = 100, include_low_quality: bool = False) -> str:
|
||||
"""תור ההלכות הממתינות לאישור (review_status='pending_review').
|
||||
|
||||
כברירת-מחדל (#84.1, #84.3) התור **מסונן** — הלכות עם דגל-איכות כלשהו
|
||||
(application / ציטוט-לא-מאומת / קטוע / obiter / restatement דק / לא-נתמך /
|
||||
near-duplicate) מוסתרות (הן שייכות ל'דורש תיקון-חילוץ', לא לתור-האישור),
|
||||
ו**ממוין לפי עדיפות** (טופלו-לרעה תחילה, אז הכי לא-ודאיים, אז הישנים).
|
||||
|
||||
Args:
|
||||
limit: מספר מקסימלי.
|
||||
include_low_quality: True כדי לחשוף גם פריטים מסומני-איכות (בקט 'דורש תיקון').
|
||||
"""
|
||||
rows = await db.list_halachot(
|
||||
review_status="pending_review",
|
||||
limit=limit,
|
||||
exclude_low_quality=not include_low_quality,
|
||||
order_by_priority=True,
|
||||
)
|
||||
return _ok(rows)
|
||||
|
||||
@@ -6031,7 +6031,13 @@ async def halachot_list(
|
||||
practice_area: str = "",
|
||||
limit: int = 200,
|
||||
offset: int = 0,
|
||||
exclude_low_quality: bool = False,
|
||||
order_by_priority: bool = False,
|
||||
):
|
||||
"""List halachot. ``exclude_low_quality`` hides flagged items (#84.1) and
|
||||
``order_by_priority`` switches to the active-learning order (#84.3). Both
|
||||
default off so existing callers are unaffected; the review-queue view opts
|
||||
in."""
|
||||
cid: UUID | None = None
|
||||
if case_law_id:
|
||||
try:
|
||||
@@ -6043,6 +6049,8 @@ async def halachot_list(
|
||||
review_status=review_status or None,
|
||||
practice_area=practice_area or None,
|
||||
limit=limit, offset=offset,
|
||||
exclude_low_quality=exclude_low_quality,
|
||||
order_by_priority=order_by_priority,
|
||||
)
|
||||
return {"items": rows, "count": len(rows)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user