feat(halacha): equivalent-halacha (parallel-authority) links across precedents
Cross-precedent recurrence of a principle is real but is NOT citation
corroboration (X11) — the 5 candidate pairs have ZERO citations between their
precedents. Recording them in halacha_citation_corroboration would fabricate
citation data and inflate corroboration_count. This adds a proper, separate
halacha-level link for parallel authority.
Schema (V28): equivalent_halachot — symmetric (halacha_a < halacha_b, CHECK +
UNIQUE), non-citation, cross-precedent-only. ON DELETE CASCADE.
db.py:
- link_equivalent_halachot (idempotent; rejects same-id and SAME-precedent pairs
— parallel authority is cross-precedent by definition), unlink, and
list_equivalent_for_halacha.
- list_halachot gains include_equivalents → _annotate_equivalents attaches an
`equivalents` list (both directions) per row.
API: include_equivalents on GET /api/halachot; GET/POST/DELETE
/api/halachot/{id}/equivalents for the chair to view/link/unlink manually.
scripts/halacha_batch_reconcile.py: --link records found cross-precedent pairs
as equivalent_halachot (non-destructive, idempotent).
web-ui: Halacha.equivalents type; the clean review queue fetches
include_equivalents; the review card shows a gold "עיקרון מקביל ב-N" badge + an
expandable list (case + rule + similarity) labeled "אסמכתה מקבילה — לא ציטוט".
Populated the 5 reviewed pairs (chair decision: keep all + link as parallel
authority). Verified: 5 rows; the 1023-20 hub annotates 3 of its halachot with
equivalents; tsc --noEmit exits 0.
Invariants: G1 (model recurrence at source in its own table, not by abusing the
citator); G2 (no parallel path — extends list_halachot); citator integrity
preserved (corroboration stays citation-only).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
46
web/app.py
46
web/app.py
@@ -6034,10 +6034,12 @@ async def halachot_list(
|
||||
exclude_low_quality: bool = False,
|
||||
order_by_priority: bool = False,
|
||||
cluster: bool = False,
|
||||
include_equivalents: bool = False,
|
||||
):
|
||||
"""List halachot. ``exclude_low_quality`` hides flagged items (#84.1),
|
||||
``order_by_priority`` switches to the active-learning order (#84.3), and
|
||||
``cluster`` annotates near-duplicate groups for one-card review (#84.2). All
|
||||
``order_by_priority`` switches to the active-learning order (#84.3),
|
||||
``cluster`` annotates near-duplicate groups for one-card review (#84.2), and
|
||||
``include_equivalents`` attaches cross-precedent parallel-authority links. All
|
||||
default off so existing callers are unaffected; the review queue opts in."""
|
||||
cid: UUID | None = None
|
||||
if case_law_id:
|
||||
@@ -6053,10 +6055,50 @@ async def halachot_list(
|
||||
exclude_low_quality=exclude_low_quality,
|
||||
order_by_priority=order_by_priority,
|
||||
cluster=cluster,
|
||||
include_equivalents=include_equivalents,
|
||||
)
|
||||
return {"items": rows, "count": len(rows)}
|
||||
|
||||
|
||||
class EquivalentLinkRequest(BaseModel):
|
||||
other_id: str
|
||||
note: str = ""
|
||||
|
||||
|
||||
@app.get("/api/halachot/{halacha_id}/equivalents")
|
||||
async def halacha_equivalents_list(halacha_id: str):
|
||||
"""Cross-precedent parallel-authority links for a halacha (#84.2)."""
|
||||
try:
|
||||
hid = UUID(halacha_id)
|
||||
except ValueError:
|
||||
raise HTTPException(400, "halacha_id לא תקין")
|
||||
return {"items": await db.list_equivalent_for_halacha(hid)}
|
||||
|
||||
|
||||
@app.post("/api/halachot/{halacha_id}/equivalents")
|
||||
async def halacha_equivalents_link(halacha_id: str, req: EquivalentLinkRequest):
|
||||
"""Chair links two halachot as the same principle across precedents (#84.2)."""
|
||||
try:
|
||||
hid = UUID(halacha_id)
|
||||
oid = UUID(req.other_id)
|
||||
except ValueError:
|
||||
raise HTTPException(400, "מזהה הלכה לא תקין")
|
||||
ok = await db.link_equivalent_halachot(hid, oid, note=req.note, created_by="chair")
|
||||
if not ok:
|
||||
raise HTTPException(
|
||||
400, "לא ניתן לקשר — אותה הלכה או שתי הלכות מאותו פסק (קישור-מקביל הוא חוצה-פסקים)")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.delete("/api/halachot/{halacha_id}/equivalents/{other_id}")
|
||||
async def halacha_equivalents_unlink(halacha_id: str, other_id: str):
|
||||
try:
|
||||
hid, oid = UUID(halacha_id), UUID(other_id)
|
||||
except ValueError:
|
||||
raise HTTPException(400, "מזהה הלכה לא תקין")
|
||||
return {"ok": await db.unlink_equivalent_halachot(hid, oid)}
|
||||
|
||||
|
||||
@app.patch("/api/halachot/{halacha_id}")
|
||||
async def halacha_update(halacha_id: str, req: HalachaUpdateRequest):
|
||||
"""Approve / reject / edit a halacha. Used by the chair review queue."""
|
||||
|
||||
Reference in New Issue
Block a user