-
{current.label}
+
+
{current.label}
+ {CHECKLIST_APPLIES[current.key] && (
+
+ חל על: {CHECKLIST_APPLIES[current.key]}
+
+ )}
+
setPreview(!preview)}
- className="text-xs"
+ className="text-xs shrink-0"
>
{preview ? : }
{preview ? "עריכה" : "תצוגה מקדימה"}
diff --git a/web-ui/src/components/precedents/library-search-panel.tsx b/web-ui/src/components/precedents/library-search-panel.tsx
index 6c99464..22d8970 100644
--- a/web-ui/src/components/precedents/library-search-panel.tsx
+++ b/web-ui/src/components/precedents/library-search-panel.tsx
@@ -13,6 +13,7 @@ import {
useLibrarySearch, type PracticeArea, type SearchHit,
} from "@/lib/api/precedent-library";
import { PRACTICE_AREAS, PRECEDENT_LEVELS } from "./practice-area";
+import { AuthorityBadge } from "./halacha-meta";
function formatDate(iso: string | null) {
if (!iso) return "—";
@@ -32,6 +33,10 @@ function HalachaCard({ hit }: { hit: Extract })
{hit.court && · {hit.court} }
{hit.decision_date && · {formatDate(hit.decision_date)} }
{hit.precedent_level && · {hit.precedent_level} }
+ {/* PRE-3/PRE-5 (INV-IA5): the derived authority (binding/persuasive)
+ rides on the wire but was dropped here — render it as in the review
+ tab so search shows the same provenance everywhere. */}
+
דירוג {hit.score.toFixed(2)}
diff --git a/web-ui/src/components/training/compare-panel.tsx b/web-ui/src/components/training/compare-panel.tsx
index 6d78d77..11644cd 100644
--- a/web-ui/src/components/training/compare-panel.tsx
+++ b/web-ui/src/components/training/compare-panel.tsx
@@ -118,7 +118,15 @@ export function ComparePanel() {
const { data: corpus, isPending } = useCorpus();
const [a, setA] = useState(null);
const [b, setB] = useState(null);
- const cmp = useCompare(a, b);
+
+ // LRN-6 (INV-IA2): if a selected decision was deleted from the corpus on
+ // another surface, a cached selection would POST a stale id and 404. Derive
+ // the effective selection from the refreshed corpus instead of holding it in
+ // state — a deleted id resolves to null (no effect, no stale POST).
+ const ids = corpus ? new Set(corpus.map((c) => c.id)) : null;
+ const validA = a && (!ids || ids.has(a)) ? a : null;
+ const validB = b && (!ids || ids.has(b)) ? b : null;
+ const cmp = useCompare(validA, validB);
return (
@@ -133,7 +141,7 @@ export function ComparePanel() {
(slot === "a" ? setA(v) : setB(v))}
dir="rtl"
>
@@ -155,7 +163,7 @@ export function ComparePanel() {
- {a && b && a === b && (
+ {validA && validB && validA === validB && (
בחר שתי החלטות שונות
)}
@@ -167,7 +175,7 @@ export function ComparePanel() {
)}
- {cmp.isPending && a && b && a !== b && (
+ {cmp.isPending && validA && validB && validA !== validB && (
)}
diff --git a/web-ui/src/components/training/curator-portrait-panel.tsx b/web-ui/src/components/training/curator-portrait-panel.tsx
index 840c7da..670ccdd 100644
--- a/web-ui/src/components/training/curator-portrait-panel.tsx
+++ b/web-ui/src/components/training/curator-portrait-panel.tsx
@@ -83,7 +83,7 @@ function StatsCard() {
} />
} />
- } />
+ } />
0
diff --git a/web-ui/src/components/training/style-report-panel.tsx b/web-ui/src/components/training/style-report-panel.tsx
index f165ab0..05a46d9 100644
--- a/web-ui/src/components/training/style-report-panel.tsx
+++ b/web-ui/src/components/training/style-report-panel.tsx
@@ -83,10 +83,13 @@ export function StyleReportPanel() {
label="ממוצע להחלטה"
value={`${(c.avg_chars / 1000).toFixed(1)}K`}
/>
+ {/* LRN-4 (INV-IA5): items.length (style_patterns freq>0) and
+ contribution.total_patterns are independent queries — the old
+ "X מתוך Y" framed a false subset. Show the surfaced count alone. */}
diff --git a/web-ui/src/lib/api/exports.ts b/web-ui/src/lib/api/exports.ts
index bce36ed..3ff5e25 100644
--- a/web-ui/src/lib/api/exports.ts
+++ b/web-ui/src/lib/api/exports.ts
@@ -5,6 +5,7 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "./client";
import { casesKeys } from "./cases";
+import { decisionBlocksKeys } from "./decision-blocks";
export type ExportFile = {
filename: string;
@@ -80,6 +81,9 @@ export function useExportDocx(caseNumber: string) {
onSuccess: () => {
qc.invalidateQueries({ queryKey: exportsKeys.list(caseNumber) });
qc.invalidateQueries({ queryKey: casesKeys.detail(caseNumber) });
+ // CAS-2 (INV-IA2): export flips active_draft → source_of_truth='docx';
+ // the block viewer must re-fetch or its sync warning stays hidden.
+ qc.invalidateQueries({ queryKey: decisionBlocksKeys.case(caseNumber) });
},
});
}
@@ -104,6 +108,9 @@ export function useUploadDraft(caseNumber: string) {
qc.invalidateQueries({ queryKey: exportsKeys.list(caseNumber) });
qc.invalidateQueries({ queryKey: exportsKeys.activeDraft(caseNumber) });
qc.invalidateQueries({ queryKey: exportsKeys.bookmarks(caseNumber) });
+ // CAS-1 (INV-IA2): uploading a DOCX draft makes it the source-of-truth;
+ // refresh the block viewer so the divergence banner appears immediately.
+ qc.invalidateQueries({ queryKey: decisionBlocksKeys.case(caseNumber) });
},
});
}
diff --git a/web-ui/src/lib/api/feedback.ts b/web-ui/src/lib/api/feedback.ts
index b749b8b..aeb9da4 100644
--- a/web-ui/src/lib/api/feedback.ts
+++ b/web-ui/src/lib/api/feedback.ts
@@ -79,6 +79,9 @@ export function useCreateFeedback() {
}),
onSuccess: () => {
qc.invalidateQueries({ queryKey: feedbackKeys.all });
+ // APR-4 (INV-IA2): a new unresolved comment raises the chair-pending
+ // aggregate — keep /approvals + the nav badge in sync.
+ qc.invalidateQueries({ queryKey: ["chair", "pending"] });
},
});
}
@@ -103,6 +106,9 @@ export function useResolveFeedback() {
),
onSuccess: () => {
qc.invalidateQueries({ queryKey: feedbackKeys.all });
+ // APR-1/APR-4 (INV-IA2): resolving a comment lowers the chair-pending
+ // aggregate; refresh /approvals' counter + the nav badge (one source).
+ qc.invalidateQueries({ queryKey: ["chair", "pending"] });
},
});
}
diff --git a/web-ui/src/lib/api/learning.ts b/web-ui/src/lib/api/learning.ts
index f2ed7e7..345dd8d 100644
--- a/web-ui/src/lib/api/learning.ts
+++ b/web-ui/src/lib/api/learning.ts
@@ -5,6 +5,8 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "./client";
+import { trainingKeys } from "./training";
+import { methodologyKeys } from "./methodology";
export type DraftFinalPair = {
id: string;
@@ -111,6 +113,13 @@ export function usePromoteLearning(pairId: string) {
),
onSuccess: () => {
qc.invalidateQueries({ queryKey: learningKeys.all });
+ // LRN-10 (INV-IA2): promote flips folded lessons' review_status, so the
+ // per-corpus LessonsTab must refetch (lessons key is corpus-scoped —
+ // invalidate the whole "lessons" prefix since promote is pair-scoped).
+ qc.invalidateQueries({ queryKey: [...trainingKeys.all, "lessons"] });
+ // MET-1 (INV-IA2): promote also writes discussion_rules + transition_
+ // phrases['universal'] — /methodology (staleTime 30s) would stay stale.
+ qc.invalidateQueries({ queryKey: methodologyKeys.all });
},
});
}
diff --git a/web-ui/src/lib/api/missing-precedents.ts b/web-ui/src/lib/api/missing-precedents.ts
index 2980f31..7b58ab7 100644
--- a/web-ui/src/lib/api/missing-precedents.ts
+++ b/web-ui/src/lib/api/missing-precedents.ts
@@ -158,6 +158,10 @@ export function useCreateMissingPrecedent() {
}),
onSuccess: () => {
qc.invalidateQueries({ queryKey: missingPrecedentKeys.all });
+ // APR-6 / ADM-3 (INV-IA2): a new open gap raises the chair-pending
+ // aggregate and the /operations missing_precedents counter.
+ qc.invalidateQueries({ queryKey: ["chair", "pending"] });
+ qc.invalidateQueries({ queryKey: ["operations"] });
},
});
}
@@ -173,6 +177,10 @@ export function useUpdateMissingPrecedent() {
onSuccess: (_, { id }) => {
qc.invalidateQueries({ queryKey: missingPrecedentKeys.detail(id) });
qc.invalidateQueries({ queryKey: missingPrecedentKeys.all });
+ // APR-6 / ADM-3 (INV-IA2): a status edit (e.g. open→irrelevant) shifts
+ // the chair-pending aggregate and the /operations counter.
+ qc.invalidateQueries({ queryKey: ["chair", "pending"] });
+ qc.invalidateQueries({ queryKey: ["operations"] });
},
});
}
@@ -187,6 +195,10 @@ export function useDeleteMissingPrecedent() {
),
onSuccess: () => {
qc.invalidateQueries({ queryKey: missingPrecedentKeys.all });
+ // APR-6 / ADM-3 (INV-IA2): removing a gap lowers the chair-pending
+ // aggregate and the /operations counter.
+ qc.invalidateQueries({ queryKey: ["chair", "pending"] });
+ qc.invalidateQueries({ queryKey: ["operations"] });
},
});
}
@@ -256,6 +268,10 @@ export function useUploadMissingPrecedent() {
qc.invalidateQueries({ queryKey: missingPrecedentKeys.detail(id) });
qc.invalidateQueries({ queryKey: missingPrecedentKeys.all });
qc.invalidateQueries({ queryKey: ["precedent-library"] });
+ // APR-6 / ADM-3 (INV-IA2): uploading closes the gap → chair-pending
+ // aggregate and the /operations missing_precedents counter both drop.
+ qc.invalidateQueries({ queryKey: ["chair", "pending"] });
+ qc.invalidateQueries({ queryKey: ["operations"] });
},
});
}
diff --git a/web-ui/src/lib/api/precedent-library.ts b/web-ui/src/lib/api/precedent-library.ts
index bc9dbbc..156d174 100644
--- a/web-ui/src/lib/api/precedent-library.ts
+++ b/web-ui/src/lib/api/precedent-library.ts
@@ -646,6 +646,10 @@ export function useUpdateHalacha() {
),
onSuccess: () => {
qc.invalidateQueries({ queryKey: libraryKeys.all });
+ // APR-5 / ADM-2 (INV-IA2): approving/rejecting a halacha changes the
+ // chair-pending aggregate and the /operations halacha_review counter.
+ qc.invalidateQueries({ queryKey: ["chair", "pending"] });
+ qc.invalidateQueries({ queryKey: ["operations"] });
},
});
}
@@ -666,6 +670,10 @@ export function useBatchReviewHalachot() {
),
onSuccess: () => {
qc.invalidateQueries({ queryKey: libraryKeys.all });
+ // APR-5 / ADM-2 (INV-IA2): a batch review shifts the chair-pending
+ // aggregate and the /operations halacha_review counter.
+ qc.invalidateQueries({ queryKey: ["chair", "pending"] });
+ qc.invalidateQueries({ queryKey: ["operations"] });
},
});
}
diff --git a/web-ui/src/lib/api/system.ts b/web-ui/src/lib/api/system.ts
index 2ea9d29..c1115a8 100644
--- a/web-ui/src/lib/api/system.ts
+++ b/web-ui/src/lib/api/system.ts
@@ -18,9 +18,28 @@ export type DiagDoc = {
created_at: string | null;
};
+/** Halacha review backlog (GAP-14 / INV-QA1 / G10) — human-gate visibility. */
+export type HalachaBacklog = {
+ pending_review: number;
+ pending_clean: number;
+ pending_flagged: number;
+ approved: number;
+ rejected: number;
+ deferred: number;
+ published: number;
+ total: number;
+ reviewed_total: number;
+ oldest_pending_at: string | null;
+ throughput_24h: number;
+ throughput_7d: number;
+};
+
export type Diagnostics = {
db_ok: boolean;
tables: Record;
+ // ADM-1 (INV-IA5): the backend returns this human-gate counter; render it
+ // rather than silently dropping it. /approvals owns the action (INV-IA1).
+ halacha_backlog: HalachaBacklog;
failed_documents: DiagDoc[];
stuck_documents: DiagDoc[];
active_tasks: Array<{
diff --git a/web-ui/src/lib/api/training.ts b/web-ui/src/lib/api/training.ts
index 2501873..f531472 100644
--- a/web-ui/src/lib/api/training.ts
+++ b/web-ui/src/lib/api/training.ts
@@ -172,6 +172,10 @@ export function useDeleteCorpusEntry() {
onSuccess: () => {
qc.invalidateQueries({ queryKey: trainingKeys.corpus() });
qc.invalidateQueries({ queryKey: trainingKeys.report() });
+ // LRN-8 (INV-IA2): deleting a corpus row nulls style_corpus_id on any
+ // chat that referenced it — refresh the conversation list so those
+ // chats no longer show a stale corpus link.
+ qc.invalidateQueries({ queryKey: chatKeys.conversations() });
},
});
}
@@ -301,7 +305,10 @@ export type CuratorStats = {
total_findings: number;
decisions_with_findings: number;
decisions_total: number;
- findings_applied: number;
+ // LRN-5 (INV-IA5): the count of curator lessons with review_status='approved'
+ // (the real INV-LRN1 writer gate) — replaces the old findings_applied, which
+ // counted the informative-only applied_to_skill flag.
+ findings_approved: number;
recent_findings: CuratorFinding[];
};
diff --git a/web/app.py b/web/app.py
index 491e3ed..a323636 100644
--- a/web/app.py
+++ b/web/app.py
@@ -1327,9 +1327,13 @@ async def get_curator_stats():
"WHERE source = 'curator'"
)
total_corpus = await conn.fetchval("SELECT count(*) FROM style_corpus")
- applied = await conn.fetchval(
+ # LRN-5 (INV-IA5): count the *real* consumer-mapped gate — review_status
+ # 'approved' is what flows to the writer (INV-LRN1, #126). The old count
+ # of applied_to_skill was a KPI over an informative-only flag (LRN-1)
+ # that writes nowhere, so it reported adoption that never happened.
+ approved = await conn.fetchval(
"SELECT count(*) FROM decision_lessons "
- "WHERE source = 'curator' AND applied_to_skill = true"
+ "WHERE source = 'curator' AND review_status = 'approved'"
)
# Last 10 curator findings — newest first
recent_rows = await conn.fetch(
@@ -1348,7 +1352,7 @@ async def get_curator_stats():
"total_findings": total_lessons or 0,
"decisions_with_findings": decisions_with_findings or 0,
"decisions_total": total_corpus or 0,
- "findings_applied": applied or 0,
+ "findings_approved": approved or 0,
"recent_findings": [
{
"id": str(r["id"]),
@@ -6344,14 +6348,10 @@ async def precedent_request_halachot(case_law_id: str):
return {"queued": True, "case_law_id": case_law_id, "kind": "halacha", "wakeup": wakeup}
-@app.get("/api/precedent-library/queue/pending")
-async def precedent_queue_pending(kind: str = "metadata", limit: int = 20):
- """Read-only view of the queue. The MCP worker reads this too, but the
- UI calls it to show 'X ממתינות לעיבוד מקומי' badges."""
- if kind not in {"metadata", "halacha"}:
- raise HTTPException(400, "kind חייב להיות metadata או halacha")
- items = await db.list_pending_extraction_requests(kind=kind, limit=limit)
- return {"items": items, "count": len(items)}
+# PRE-2 (INV-IA1/dead-surface): removed GET /api/precedent-library/queue/pending —
+# it had zero frontend consumers (its docstring's "the UI calls it" claim was
+# stale; only /api/digests/queue/pending is consumed). The MCP worker reads the
+# queue via db.list_pending_extraction_requests directly, not over HTTP.
# ── Digests radar (X12) — secondary discovery layer ─────────────────