File size: 5,928 Bytes
ab5ea78 49dfe4d 0466f67 49dfe4d ab5ea78 6f5d619 ab5ea78 6f5d619 ab5ea78 6f5d619 ab5ea78 6f5d619 49dfe4d ab5ea78 0466f67 ab5ea78 6f5d619 ab5ea78 6f5d619 ab5ea78 6f5d619 b5de3b4 6f5d619 0466f67 6f5d619 ab5ea78 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | """The frequency-sorted review queue — the pipeline's actual product.
Ordering is the product decision here, and it targets the bottleneck directly:
the expert is the scarce resource, so they should hit the terms whose definition
propagates furthest first. Conflicts are promoted above frequency regardless,
because a contradiction is a decision only they can make.
Each row carries page, section and the verbatim span so review is a matter of
checking a quote against a page, not a claim against memory. That is what makes
the queue finishable.
Rows carry BOTH `page` (0-based, as the parser reports) and `page_no` (1-based,
what a human reads). A review UI must render `page_no`: an off-by-one here is
invisible until an expert opens the wrong page and concludes the provenance is
wrong.
v2 dropped `extraction_status`, `diff_status`, `definition_conflict` and
`conflict_variants` from the row: all four are already expressed by
`review_reason`, which is the field a reviewer actually reads, and they remain
on the entry for a consumer that wants them.
"""
from __future__ import annotations
def build_queue(entries: list[dict], domain: dict | None = None) -> list[dict]:
"""Rows an expert works through, most-decision-worthy first.
**The queue is no longer glossary-only.** Every row carries `row_kind`, and
a consumer must switch on it rather than assume `term`: an unresolved key
parameter has a surface and no term, no definition and no mention count.
"""
def sort_key(entry: dict):
conflicting = (
entry.get("diff_status") == "conflicting"
or entry.get("definition_conflict") is True
)
return (0 if conflicting else 2, -int(entry.get("mention_count", 0) or 0))
queue = []
for entry in sorted(entries, key=sort_key):
provenance = entry.get("provenance") or {}
queue.append(
{
"row_kind": "term",
# What an approve/edit/reject decision attaches to. Without it a
# re-run loses which rows the expert already cleared.
"term_id": entry.get("term_id"),
"term": entry.get("term"),
"definition": entry.get("definition"),
"source_wording": entry.get("source_wording"),
"mention_count": entry.get("mention_count", 0),
"page": provenance.get("page"),
# 1-based, the number a reviewer reads. `page` stays 0-based.
"page_no": provenance.get("page_no"),
"section_no": provenance.get("section_no"),
"span": provenance.get("span"),
"review_reason": _reason(entry),
"_sort": sort_key(entry),
}
)
queue.extend(_key_parameter_rows(domain))
# Ranked once, over both kinds. Unresolved parameters sit between conflicts
# and routine confirmations: a contradiction is still the more urgent call,
# but "the model named a parameter nothing in the document corroborates" is
# a judgement only the expert can make, and it should not sink beneath 60
# routine rows where nobody reaches it.
queue.sort(key=lambda row: row["_sort"])
for rank, row in enumerate(queue, start=1):
row["rank"] = rank
del row["_sort"]
return queue
def _key_parameter_rows(domain: dict | None) -> list[dict]:
"""One row per key parameter that resolved to no extracted term.
Resolved parameters produce no row — they are corroborated and need nothing
from a human. Only the dangles are worth an expert's attention, and they are
worth it precisely because this branch has no span check: an unresolvable
surface is the one hallucination signal it can produce.
"""
provenance = (domain or {}).get("provenance") or {}
rows = []
for parameter in (domain or {}).get("key_parameters") or []:
if parameter.get("term_id"):
continue
rows.append(
{
"row_kind": "key_parameter",
# The decision attaches to the domain entry plus the surface —
# there is no term_id, which is the entire point of the row.
"term_id": None,
"brief_id": (domain or {}).get("brief_id"),
"term": parameter.get("surface"),
"definition": None,
"source_wording": None,
"mention_count": 0,
"page": provenance.get("page"),
"page_no": provenance.get("page_no"),
"section_no": provenance.get("section_no"),
"span": provenance.get("span"),
"review_reason": (
"named as a key parameter but no extracted term corroborates it "
"— confirm it is real"
),
"_sort": (1, 0),
}
)
return rows
def _reason(entry: dict) -> str:
if entry.get("definition_conflict") or entry.get("diff_status") == "conflicting":
return "conflicting definitions — expert decision required"
if _wording_differs(entry):
return "source wording differs from the expanded name — confirm which is correct"
if entry.get("extraction_status") == "no_definition_found":
return "term found but no definition in document"
if not entry.get("definition"):
return "definition rejected by span check or absent"
return "routine confirmation"
def _wording_differs(entry: dict) -> bool:
"""The document says "Physical of Availability"; the expansion says
"Physical Availability". Surfacing that to the expert is a locked
requirement, so it earns its own review reason."""
source = (entry.get("source_wording") or "").strip().casefold()
full = (entry.get("full_name") or "").strip().casefold()
return bool(source and full) and full not in source
|