feat(halacha-triage UI): wire gating + near-duplicate cluster cards (#84.2)
Completes #84 — surfaces the backend gating/prioritization (#84.1/#84.3, PR #93) in the chair's review UI and adds near-duplicate clustering (#84.2). Backend - db.list_halachot gains `cluster` (#84.2): annotates each row with cluster_id + cluster_size by unioning same-precedent halachot within HALACHA_CLUSTER_COSINE (0.90, new config). Display-only — never merges/deletes. Pairwise is confined to the returned set (cheap). - GET /api/halachot exposes the `cluster` query param (default off). Frontend (web-ui) - Halacha type gains optional cluster_id / cluster_size (hand-written module; no api:types regen needed — halachot aren't typed off the generated schema). - useHalachotPending(opts): the default "clean" queue now fetches exclude_low_quality + order_by_priority + cluster; needsFix:true returns the flagged 'needs extraction fix' bucket (filtered client-side). - HalachaReviewPanel: a "תור נקי / דורש תיקון-חילוץ" toggle (#84.1); near-dup clusters collapse into ONE card showing "+N וריאנטים" with an expandable list, and approve/reject/defer on a clustered card applies to all variants via the batch endpoint (#84.2 + #84.4). Counts show true halacha totals (pendingTotal). New flag labels added (application / near_duplicate / nevo_preamble_leak). Verified: - backend: list_halachot(cluster=True) on the live queue — algorithm correct (groups related same-precedent rules at 0.78; none at the production 0.90 because dedup #82 already removed near-dups — the desired state). - frontend: `tsc --noEmit` exits 0 (type-clean); no new lint errors (the one lint error is pre-existing in training/learning-panel.tsx from #94). Local Turbopack build can't run on the worktree node_modules symlink — CI builds in a clean checkout. Invariants: G1 (gate/cluster at source in SQL, not post-hoc); G2 (same list_halachot path); §6 (flagged items routed to a visible bucket, not dropped). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -92,6 +92,11 @@ export type Halacha = {
|
||||
* negatively (distinguished/criticized/overruled). */
|
||||
corroboration_count?: number;
|
||||
corroboration_negative?: boolean;
|
||||
/* #84.2 near-duplicate clustering (present only when fetched with cluster=true):
|
||||
* same-precedent halachot within the cluster cosine share a cluster_id, so the
|
||||
* UI collapses them into one review card. cluster_size === 1 → singleton. */
|
||||
cluster_id?: string;
|
||||
cluster_size?: number;
|
||||
};
|
||||
|
||||
export type RelatedCase = {
|
||||
@@ -566,14 +571,32 @@ export function useRequestHalachotExtraction() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useHalachotPending(limit = 200) {
|
||||
/** #84.1/#84.2/#84.3 — the chair review queue.
|
||||
*
|
||||
* Default ("clean") view: quality-gated (flagged items hidden), priority-ordered
|
||||
* (most-uncertain/negatively-treated first), and near-duplicate-clustered into
|
||||
* one card. Pass `needsFix: true` for the 'needs extraction fix' bucket — every
|
||||
* pending item carrying a quality flag (filtered client-side). */
|
||||
export function useHalachotPending(
|
||||
opts: { limit?: number; needsFix?: boolean } = {},
|
||||
) {
|
||||
const { limit = 200, needsFix = false } = opts;
|
||||
const qs = needsFix
|
||||
? `review_status=pending_review&exclude_low_quality=false&limit=${limit}`
|
||||
: `review_status=pending_review&exclude_low_quality=true`
|
||||
+ `&order_by_priority=true&cluster=true&limit=${limit}`;
|
||||
return useQuery({
|
||||
queryKey: libraryKeys.halachotPending(),
|
||||
queryFn: ({ signal }) =>
|
||||
apiRequest<{ items: Halacha[]; count: number }>(
|
||||
`/api/halachot?review_status=pending_review&limit=${limit}`,
|
||||
queryKey: [...libraryKeys.halachotPending(), needsFix ? "needsfix" : "clean"],
|
||||
queryFn: async ({ signal }) => {
|
||||
const res = await apiRequest<{ items: Halacha[]; count: number }>(
|
||||
`/api/halachot?${qs}`,
|
||||
{ signal },
|
||||
),
|
||||
);
|
||||
if (!needsFix) return res;
|
||||
// needs-fix bucket = pending items that carry a quality flag
|
||||
const items = res.items.filter((h) => (h.quality_flags?.length ?? 0) > 0);
|
||||
return { items, count: items.length };
|
||||
},
|
||||
staleTime: 5_000,
|
||||
refetchOnMount: "always",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user