fix(corpus): committee decisions are persuasive, never binding (INV-DM7) #306
@@ -35,14 +35,14 @@
|
||||
| `case_number` (citation) | CHAIR (חובה) | מפתח idempotency |
|
||||
| `full_text`, `extraction_status`, `source_kind` | DETERMINISTIC | — |
|
||||
| `case_name`, `court`, `date`, `headnote`, `summary`, `key_quote`, `subject_tags`, `appeal_subtype`, `precedent_level`, `source_type`, `citation_formatted` | CHAIR או OPUS | Opus ממלא רק אם ריק |
|
||||
| `is_binding` | CHAIR (default true) | קובע prompt-הלכה |
|
||||
| `is_binding` | CHAIR (default true) **אך** DETERMINISTIC-false ל-`source_type=appeals_committee` | קובע prompt-הלכה. סמכות מבנית ([INV-DM7](02-data-model.md#inv-dm7-סיווג-הלכה--סמכות-נגזרת--תפקיד-כלל-מסווג-שני-צירים-לא-enum-אחד)): מקור-ועדה הוא תמיד persuasive — נכפה ל-false ב-`create_external_case_law` ונאכף ע"י `case_law_committee_not_binding_check` |
|
||||
| chunks (`content`/`section_type`/`page_number`) | DETERMINISTIC | — |
|
||||
| `embedding` (chunks) | Voyage (לא-LLM-reasoning) | ⚠ לא-GENERATED ([gap-audit GAP-09](gap-audit.md)) |
|
||||
| כל `halachot` | OPUS | נכנס pending_review |
|
||||
|
||||
### 2ב. החלטה פנימית (`case_law`, source_kind=`internal_committee`)
|
||||
כמו 2א, ובנוסף: `case_number` **חובה**; `chair_name`/`district`/`proceeding_type` — CHAIR או OPUS או DERIVED;
|
||||
`source_type` = `appeals_committee` (DETERMINISTIC קבוע). placeholder `"(טרם חולץ)"` מסומן ל-chair_name/district
|
||||
`source_type` = `appeals_committee` (DETERMINISTIC קבוע); `is_binding` = `false` (DETERMINISTIC קבוע — נכפה ב-`create_internal_committee_decision`, [INV-DM7](02-data-model.md#inv-dm7-סיווג-הלכה--סמכות-נגזרת--תפקיד-כלל-מסווג-שני-צירים-לא-enum-אחד): החלטת-ועדה אינה מחייבת ועדה אחרת ולא את עצמה — תמיד persuasive). placeholder `"(טרם חולץ)"` מסומן ל-chair_name/district
|
||||
ריקים ומטופל כריק ע"י ה-extractor.
|
||||
|
||||
### 2ג. מסמך-תיק (`documents`)
|
||||
|
||||
@@ -1669,6 +1669,27 @@ CREATE INDEX IF NOT EXISTS idx_hcc_canonical
|
||||
"""
|
||||
|
||||
|
||||
SCHEMA_V42_SQL = """
|
||||
-- INV-DM7: authority (binding/persuasive) is STRUCTURAL for committee sources.
|
||||
-- An appeals-committee decision (source_kind='internal_committee', or any row
|
||||
-- with source_type='appeals_committee') is persuasive, NEVER binding — a
|
||||
-- committee's ruling does not bind another committee, nor itself. is_binding
|
||||
-- stays a stored column (it selects the halacha-extraction prompt), but for
|
||||
-- committee sources it is a faithful cache of the derived value, not a chair
|
||||
-- guess. Mirrors 02-data-model.md §INV-DM7 / X8-field-provenance.md.
|
||||
-- Step 1 — normalize legacy rows at the source (G1), idempotent.
|
||||
UPDATE case_law SET is_binding = FALSE
|
||||
WHERE is_binding IS TRUE
|
||||
AND (source_kind = 'internal_committee' OR source_type = 'appeals_committee');
|
||||
-- Step 2 — constrain so a binding committee row can never be written again.
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE case_law ADD CONSTRAINT case_law_committee_not_binding_check
|
||||
CHECK (NOT ((source_kind = 'internal_committee'
|
||||
OR source_type = 'appeals_committee') AND is_binding));
|
||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
"""
|
||||
|
||||
|
||||
# Stable, arbitrary key for the session-level advisory lock that serialises
|
||||
# schema DDL across processes. Every short-lived process (cron drains, services)
|
||||
# re-runs the idempotent migrations on startup; without this lock two processes
|
||||
@@ -1686,7 +1707,7 @@ async def _run_schema_migrations(pool: asyncpg.Pool) -> None:
|
||||
await _apply_schema_ddl(conn)
|
||||
finally:
|
||||
await conn.execute("SELECT pg_advisory_unlock($1)", _MIGRATION_LOCK_KEY)
|
||||
logger.info("Database schema initialized (v1-v41)")
|
||||
logger.info("Database schema initialized (v1-v42)")
|
||||
|
||||
|
||||
async def _apply_schema_ddl(conn: asyncpg.Connection) -> None:
|
||||
@@ -1732,6 +1753,7 @@ async def _apply_schema_ddl(conn: asyncpg.Connection) -> None:
|
||||
await conn.execute(SCHEMA_V39_SQL)
|
||||
await conn.execute(SCHEMA_V40_SQL)
|
||||
await conn.execute(SCHEMA_V41_SQL)
|
||||
await conn.execute(SCHEMA_V42_SQL)
|
||||
|
||||
|
||||
async def init_schema() -> None:
|
||||
@@ -4249,6 +4271,11 @@ async def create_external_case_law(
|
||||
source_kind='cited_only' (auto-discovered), promote it to
|
||||
source_kind='external_upload' and fill in the missing fields.
|
||||
"""
|
||||
# INV-DM7: an appeals-committee source is persuasive even when uploaded via
|
||||
# the external path — coerce to non-binding so it matches the committee
|
||||
# invariant and case_law_committee_not_binding_check (G1).
|
||||
if source_type == "appeals_committee":
|
||||
is_binding = False
|
||||
pool = await get_pool()
|
||||
tags_json = json.dumps(subject_tags or [], ensure_ascii=False)
|
||||
async with pool.acquire() as conn:
|
||||
@@ -4317,7 +4344,7 @@ async def create_internal_committee_decision(
|
||||
appeal_subtype: str = "",
|
||||
subject_tags: list[str] | None = None,
|
||||
summary: str = "",
|
||||
is_binding: bool = True,
|
||||
is_binding: bool = False,
|
||||
document_id: UUID | None = None,
|
||||
proceeding_type: str = "ערר",
|
||||
) -> dict:
|
||||
@@ -4327,6 +4354,10 @@ async def create_internal_committee_decision(
|
||||
exist as both 'ערר' and 'בל"מ' (an extension-of-time request can be
|
||||
filed against an existing appeal with the same number).
|
||||
"""
|
||||
# INV-DM7: authority is structural for this source — an appeals-committee
|
||||
# decision is persuasive, never binding. Coerce regardless of caller so the
|
||||
# value can never violate case_law_committee_not_binding_check (G1).
|
||||
is_binding = False
|
||||
pool = await get_pool()
|
||||
case_number = _canonical_case_number(case_number)
|
||||
tags_json = json.dumps(subject_tags or [], ensure_ascii=False)
|
||||
@@ -4470,6 +4501,12 @@ async def update_case_law(case_law_id: UUID, **fields) -> dict | None:
|
||||
"proceeding_type", "citation_formatted", "parties",
|
||||
}
|
||||
updates = {k: v for k, v in fields.items() if k in allowed}
|
||||
# INV-DM7: reclassifying a row to an appeals-committee source makes it
|
||||
# persuasive — coerce is_binding in the same patch so the UPDATE can't
|
||||
# violate case_law_committee_not_binding_check (e.g. the Gemini metadata
|
||||
# extractor relabels an external court_ruling as appeals_committee).
|
||||
if updates.get("source_type") == "appeals_committee":
|
||||
updates["is_binding"] = False
|
||||
if not updates:
|
||||
return await get_case_law(case_law_id)
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ async def _create_internal_record(**kw) -> dict:
|
||||
appeal_subtype=(kw.get("appeal_subtype") or "").strip(),
|
||||
subject_tags=list(kw.get("subject_tags") or []),
|
||||
summary=(kw.get("summary") or "").strip(),
|
||||
is_binding=kw.get("is_binding", True),
|
||||
is_binding=kw.get("is_binding", False), # INV-DM7: committee = persuasive
|
||||
document_id=kw.get("document_id"),
|
||||
proceeding_type=kw.get("proceeding_type") or "ערר",
|
||||
)
|
||||
@@ -108,7 +108,7 @@ async def ingest_internal_decision(
|
||||
appeal_subtype: str = "",
|
||||
subject_tags: list[str] | None = None,
|
||||
summary: str = "",
|
||||
is_binding: bool = True,
|
||||
is_binding: bool = False, # INV-DM7: committee sources are persuasive
|
||||
file_path: str | Path | None = None,
|
||||
text: str | None = None,
|
||||
document_id: UUID | None = None,
|
||||
@@ -229,6 +229,7 @@ async def migrate_from_external_corpus(dry_run: bool = False) -> dict:
|
||||
await conn.execute(
|
||||
"""UPDATE case_law
|
||||
SET source_kind = 'internal_committee',
|
||||
is_binding = FALSE, -- INV-DM7: committee = persuasive
|
||||
district = CASE WHEN $2 <> '' THEN $2 ELSE district END
|
||||
WHERE id = $1""",
|
||||
row["id"], district,
|
||||
|
||||
@@ -7141,7 +7141,7 @@ async def internal_decisions_upload(
|
||||
practice_area: str = Form(""),
|
||||
appeal_subtype: str = Form(""),
|
||||
subject_tags: str = Form("[]"),
|
||||
is_binding: bool = Form(True),
|
||||
is_binding: bool = Form(False), # INV-DM7: committee = persuasive (coerced at db too)
|
||||
summary: str = Form(""),
|
||||
):
|
||||
"""Upload a planning appeals-committee decision to the internal corpus.
|
||||
|
||||
Reference in New Issue
Block a user