feat(principles): decision-level panel extraction regime — cap-5 + dedup-frees-slot (Phase B, #152)

extract() routes to _extract_via_panel when HALACHA_PANEL_REGIME_ENABLED: the
3-model panel proposes → votes/score → approval rule → dedup vs corpus (known
links as citation, frees a cap slot) → cap HALACHA_PANEL_MAX_NEW genuinely-new
principles/decision (by score), rest dropped. Replaces single-model auto-approve;
legacy path kept as <2-judge fallback. db.store_panel_principles persists the
pre-decided verdict + source-aware canonical create/link (G9 reviewer=panel:...).
Dry-run validated on 29468-08-23: ~18 → 4 principles. 6 new tests; full suite 422 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-19 11:05:44 +00:00
parent a4114cce5e
commit 6b2fd562ae
4 changed files with 301 additions and 1 deletions

View File

@@ -5408,6 +5408,82 @@ async def store_halachot_for_chunk(
return inserted
async def store_panel_principles(
case_law_id: UUID, principles: list[dict],
) -> dict:
"""Persist principles selected by the tri-model panel regime (#152, Phase B).
Unlike :func:`store_halachot_for_chunk`, the verdict is ALREADY decided by the
panel — this does NOT recompute auto-approve from confidence. Each principle
carries its own ``review_status`` (approved / pending_review from the panel),
``instance_type`` ('original' = new canonical, 'citation' = link to existing),
and ``canonical_id`` (when a citation). The cap-of-5 + dedup-frees-slot is
applied by the CALLER (the extractor); this is the atomic writer.
Provenance (G9): reviewer = "panel:<voters> v<votes>/s<score>". Returns
{created_new, linked} counts.
"""
pool = await get_pool()
created_new = linked = 0
async with pool.acquire() as conn:
async with conn.transaction():
base = await conn.fetchval(
"SELECT COALESCE(MAX(halacha_index), -1) + 1 FROM halachot "
"WHERE case_law_id = $1", case_law_id,
)
for p in principles:
voters = ",".join(p.get("voters") or [])
reviewer = f"panel:{voters} v{p.get('votes', 0)}/s{p.get('score', 0)}"
approved = p.get("review_status") == "approved"
emb = p.get("embedding")
canonical_id = p.get("canonical_id")
instance_type = p.get("instance_type", "original")
idx = base + created_new + linked
await conn.execute(
"""INSERT INTO halachot
(case_law_id, halacha_index, rule_statement, rule_type,
reasoning_summary, supporting_quote, page_reference,
practice_areas, subject_tags, cites, confidence,
quote_verified, quality_flags, embedding, review_status,
reviewer, reviewed_at, canonical_id, instance_type)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,
CASE WHEN $17 THEN now() ELSE NULL END,$18,$19)""",
case_law_id, idx, p["rule_statement"],
p.get("rule_type", "interpretive"), p.get("reasoning_summary", ""),
p["supporting_quote"], p.get("page_reference", ""),
p.get("practice_areas", []), p.get("subject_tags", []),
p.get("cites", []), float(p.get("confidence", p.get("score", 0.0))),
p.get("quote_verified", False), p.get("quality_flags", []),
emb, p.get("review_status", "pending_review"), reviewer,
approved, canonical_id, instance_type,
)
if instance_type == "citation" and canonical_id is not None:
await conn.execute(
"UPDATE canonical_halachot SET "
"instance_count = instance_count + 1, updated_at = now() "
"WHERE id = $1", canonical_id,
)
linked += 1
else:
new_canon_id = await conn.fetchval(
"INSERT INTO canonical_halachot "
"(canonical_statement, rule_type, practice_areas, subject_tags, "
" embedding, first_established_in, review_status, instance_count) "
"VALUES ($1,$2,$3,$4,$5,$6,'pending_synthesis',1) RETURNING id",
p.get("rule_statement") or "", p.get("rule_type", "interpretive"),
p.get("practice_areas") or [], p.get("subject_tags") or [],
emb, case_law_id,
)
await conn.execute(
"UPDATE halachot SET canonical_id=$1 WHERE case_law_id=$2 "
"AND halacha_index=$3", new_canon_id, case_law_id, idx,
)
created_new += 1
logger.info("store_panel_principles: case_law=%s%d new, %d linked",
case_law_id, created_new, linked)
return {"created_new": created_new, "linked": linked}
async def list_halachot(
case_law_id: UUID | None = None,
review_status: str | None = None,