feat(learning): FU-2 — לכידת seed אקטיב-לרנינג בשער-היו"ר הקיים (#133)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s

כל הכרעת keep/drop חדה של היו"ר על הלכה שהפאנל כבר שפט (יש לה שורה
ב-halacha_panel_rounds) פולטת seed gold-set מתויג-יו"ר — הסיגנל היחיד
שמותר ללולאת הלמידה ללמוד ממנו. לימוד מהצבעות-הפאנל-עצמן = echo-chamber
ואסור; לכן הזרע נטבע אך-ורק מהכרעה אנושית.

- db.seed_goldset_from_chair(): capture-only, idempotent (UPSERT על
  batch='chair-live', tagged_by='chair'), לעולם לא נוגע ב-halachot ולא
  זורק שגיאה לתוך השער (INV-G10). ממפה approved/published→keep,
  rejected→drop; deferred/pending_review = נודניק, בלי seed.
- db._chair_seed_label(): שער טהור (בלי DB) → guard echo-chamber
  unit-testable; מסנן reviewer מכונה (panel:* / corroborated*).
- מחובר ב-db layer (update_halacha + update_halachot_batch) כך שכל
  מסלולי-השער מתכנסים (G1 נרמול-במקור, G2 בלי מסלול מקביל). הפאנל
  משתמש ב-SQL גולמי ולא ב-update_halacha → אין echo-chamber מבני.
- מצריך שורת-פאנל קודמת: ערך-הזרע הוא זוג (הצבעות-פאנל ⋈ הכרעת-יו"ר)
  שמזין זיקוק-rubric (FU-4) ומדידה (FU-5).
- test_chair_seed_gate.py: 10 בדיקות offline על מדיניות-השער + guard.

Invariants: INV-G10 (שער-אישור יחיד, capture-only) · INV-LRN1
(propose-only — אין auto-commit) · G1/G2 · anti-echo-chamber (#133).
אין UI/שער חדש (INV-IA). תצוגת-הצבעות-הפאנל ב-HalachaReviewPanel
(אופציונלי) נדחית — מצריכה שער-עיצוב Claude Design.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-12 04:37:12 +00:00
parent 57a6a01a03
commit 614c06ab60
2 changed files with 153 additions and 0 deletions

View File

@@ -4635,6 +4635,10 @@ async def update_halacha(
reviewed_at, created_at, updated_at
"""
row = await pool.fetchrow(sql, *params)
# FU-2 (#133): a firm chair decision on a panel-adjudicated halacha mints an
# active-learning seed. Capture-only — never blocks the gate (errors logged).
if row and review_status is not None:
await seed_goldset_from_chair(halacha_id, review_status, reviewer)
return dict(row) if row else None
@@ -4669,6 +4673,12 @@ async def update_halachot_batch(
WHERE id = ANY($1::uuid[])""",
ids, review_status, *( [reviewer] if stamp else [] ),
)
# FU-2 (#133): mint an active-learning seed for each panel-adjudicated
# halacha in the group. seed_goldset_from_chair is idempotent, self-guards
# on a prior panel round, and never raises.
if stamp:
for hid in ids:
await seed_goldset_from_chair(hid, review_status, reviewer)
try:
return int(result.split()[-1])
except (ValueError, IndexError):
@@ -5089,6 +5099,83 @@ async def insert_panel_round(
)
# The machine reviewers that DON'T represent a human ground-truth decision.
# A seed must never be minted from these (echo-chamber guard, #133). They use
# raw SQL today and never reach update_halacha, so this is defense-in-depth.
_MACHINE_REVIEWER_PREFIXES = ("panel:", "corroborated")
# Chair decisions that are a firm keep/drop judgment → map to the coarse
# is_holding gold-set axis ("is this a real, keepable rule?", the axis
# halacha_panel_calibrate.py measures against). 'deferred'/'pending_review'
# are a snooze, not a judgment → no seed.
_CHAIR_SEED_LABEL = {"approved": True, "published": True, "rejected": False}
def _chair_seed_label(review_status: str, reviewer: str = "") -> bool | None:
"""Pure gate for the FU-2 seed: the is_holding label a chair decision should
mint, or None when NO seed is allowed — either a non-firm status
(deferred/pending_review) or a machine reviewer (echo-chamber guard). Kept
pure (no DB) so the guard is unit-testable offline."""
is_holding = _CHAIR_SEED_LABEL.get(review_status)
if is_holding is None:
return None
rev = (reviewer or "").strip().lower()
if any(rev.startswith(p) for p in _MACHINE_REVIEWER_PREFIXES):
return None
return is_holding
async def seed_goldset_from_chair(
halacha_id: UUID, review_status: str, reviewer: str = "",
) -> bool:
"""Active-learning seed at the existing chair gate (#133 / FU-2).
When the chair makes a firm keep/drop decision on a halacha the panel
ALREADY adjudicated, capture it as a chair-tagged gold-set label. This is
the ONLY signal the active-learning loop is allowed to learn from — human
ground-truth, never the panel's own votes (learning from the votes is an
echo-chamber: agreement rises, accuracy doesn't, the panel drifts from the
chair). The (panel votes ⋈ chair decision) pair feeds rubric distillation
(FU-4) and measurement (FU-5).
Capture-only and idempotent (UPSERT on the chair-live batch); it never
touches `halachot` (the chair gate on /precedents stays the single source
of truth, INV-G10) and never raises into the chair gate.
Fires only when ALL hold:
- review_status is a firm decision (approved/published → keep=True,
rejected → drop=False); 'deferred'/'pending_review' are skipped.
- the reviewer is human (machine reviewers excluded — defense-in-depth).
- a prior halacha_panel_rounds row exists: the seed's value is the
panel-vs-chair pair, so a halacha the panel never judged is skipped.
Returns True iff a seed row was written/updated.
"""
is_holding = _chair_seed_label(review_status, reviewer)
if is_holding is None:
return False
try:
pool = await get_pool()
# Only seed halachot the panel previously adjudicated (FU-1 rounds).
had_round = await pool.fetchval(
"SELECT EXISTS(SELECT 1 FROM halacha_panel_rounds WHERE halacha_id = $1)",
halacha_id,
)
if not had_round:
return False
await pool.execute(
"INSERT INTO halacha_goldset (halacha_id, batch, is_holding, tagged_by, tagged_at) "
"VALUES ($1, 'chair-live', $2, 'chair', now()) "
"ON CONFLICT (halacha_id, batch) DO UPDATE "
"SET is_holding = EXCLUDED.is_holding, tagged_by = 'chair', tagged_at = now()",
halacha_id, is_holding,
)
return True
except Exception as e: # never let a learning seed break the chair gate
logger.warning("FU-2 gold-set seed failed for halacha %s: %s", halacha_id, e)
return False
async def goldset_tag(
goldset_id: UUID, *, is_holding: bool | None = None,
correct_type: str | None = None, quote_complete: bool | None = None,