Merge remote-tracking branch 'origin/feat/249-needs-review-backend' into feat/249-needs-review-ui
This commit is contained in:
@@ -40,6 +40,7 @@ from .review_schema import (
|
||||
METADATA_TABLE,
|
||||
canonical_evidence,
|
||||
evidence_digest,
|
||||
evidence_offers_choice,
|
||||
)
|
||||
|
||||
# Only this module's table constants are interpolated into the SQL below.
|
||||
@@ -96,6 +97,13 @@ _PENDING_OF_LOCATOR = (
|
||||
f"JOIN {ITEMS_TABLE} i ON i.byte_id=b.id "
|
||||
"WHERE f.locator=? AND i.state='pending'"
|
||||
)
|
||||
# Pending items of one file with their evidence: an untraced file's settle.
|
||||
_PENDING_EVIDENCE_OF_LOCATOR = (
|
||||
f"SELECT i.id,i.evidence FROM {FILES_TABLE} f " # nosec B608
|
||||
f"JOIN {BYTES_TABLE} b ON b.file_id=f.id "
|
||||
f"JOIN {ITEMS_TABLE} i ON i.byte_id=b.id "
|
||||
"WHERE f.locator=? AND i.state='pending'"
|
||||
)
|
||||
# Only an undecided item goes: a decision row (RESTRICT) is never deleted.
|
||||
_SETTLE_ITEM = (
|
||||
f"DELETE FROM {ITEMS_TABLE} WHERE id=? AND state='pending' " # nosec B608
|
||||
@@ -437,6 +445,41 @@ class ReviewOperations:
|
||||
notify_review_change(self.model.db_path)
|
||||
return removed
|
||||
|
||||
def settle_untraced_review_items(self, locators) -> int:
|
||||
"""Remove the pending, undecided items that offer nothing to choose.
|
||||
|
||||
``locators`` are files the caller found ambiguous without a choice
|
||||
(fewer than two candidates carry a prompt). Of their items, one leaves
|
||||
the queue only when its own stored evidence offers no choice either
|
||||
(``evidence_offers_choice``) and it is still ``pending`` at revision 0
|
||||
with no decision row, the same safe delete as
|
||||
``settle_linked_review_items``. Files without a pending item cost one
|
||||
indexed read and open no write transaction. Returns how many items
|
||||
were removed (the review index revision moves when any was).
|
||||
"""
|
||||
locators = list(locators)
|
||||
for locator in locators:
|
||||
_validate_locator(locator)
|
||||
conn = self.model.get_connection()
|
||||
pending = [
|
||||
locator
|
||||
for locator in locators
|
||||
if conn.execute(_PENDING_OF_LOCATOR, (locator,)).fetchone() is not None
|
||||
]
|
||||
if not pending:
|
||||
return 0
|
||||
removed = 0
|
||||
with self._pair_transaction() as conn:
|
||||
for locator in pending:
|
||||
rows = conn.execute(_PENDING_EVIDENCE_OF_LOCATOR, (locator,)).fetchall()
|
||||
for item_id, evidence in rows:
|
||||
if evidence_offers_choice(json.loads(evidence)):
|
||||
continue
|
||||
removed += conn.execute(_SETTLE_ITEM, (item_id,)).rowcount
|
||||
if removed:
|
||||
conn.execute(_BUMP_REVIEW_INDEX)
|
||||
return removed
|
||||
|
||||
def review_queue_page(self, *, limit: int, after: tuple | None = None) -> dict:
|
||||
"""One page of pending items, oldest first, from one snapshot.
|
||||
|
||||
|
||||
@@ -200,6 +200,44 @@ def canonical_evidence(value: Any) -> str:
|
||||
return text
|
||||
|
||||
|
||||
def choosable(candidate: Any) -> bool:
|
||||
"""True when a candidate offers a prompt to choose: it is ``resolved`` and
|
||||
its positive side carries non-blank text. The page disables any other
|
||||
candidate, so it is never something to choose.
|
||||
"""
|
||||
if not isinstance(candidate, dict) or candidate.get("state") != "resolved":
|
||||
return False
|
||||
positive = candidate.get("positive")
|
||||
if not isinstance(positive, dict):
|
||||
return False
|
||||
text = positive.get("text")
|
||||
return isinstance(text, str) and bool(text.strip())
|
||||
|
||||
|
||||
# An item is worth a decision only when at least this many candidates offer
|
||||
# a prompt; with fewer there is nothing to choose between (maintainer test of
|
||||
# #284/#288: 39 items whose candidates were all "unresolved").
|
||||
MIN_CHOOSABLE = 2
|
||||
|
||||
|
||||
def evidence_offers_choice(evidence: Any) -> bool:
|
||||
"""False when stored evidence provably offers nothing to choose.
|
||||
|
||||
Unlimited evidence must list ``MIN_CHOOSABLE`` choosable candidates. A
|
||||
limited item stores none, only ``candidate_total``; it may offer a choice
|
||||
(more than ``MAX_CANDIDATES`` prompts) unless that total is too small.
|
||||
"""
|
||||
if not isinstance(evidence, dict):
|
||||
return False
|
||||
if evidence.get("limited"):
|
||||
total = evidence.get("candidate_total")
|
||||
return type(total) is int and total >= MIN_CHOOSABLE
|
||||
candidates = evidence.get("candidates")
|
||||
if not isinstance(candidates, list):
|
||||
return False
|
||||
return sum(1 for c in candidates if choosable(c)) >= MIN_CHOOSABLE
|
||||
|
||||
|
||||
def evidence_digest(text: str) -> str:
|
||||
"""SHA-256 of canonical evidence text (ASCII, hence UTF-8 identical)."""
|
||||
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
||||
|
||||
Reference in New Issue
Block a user