fix(precedents): מראה-מקום never blank — seed at upload + inline Gemini enrichment (root cause)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 11s

Root cause: the upload form "מראה המקום" field is stored only as case_number;
citation_formatted was left empty and filled ONLY by the async metadata
extraction, which runs in the local drainer (not the container) because it was
lumped with halacha extraction. Result: the chair saw an empty מראה-מקום during
the window and re-typed it by hand.

Fix (hybrid — Option C):
- Seed: _create_external_record / create_external_case_law store the chair's typed
  citation as citation_formatted at INSERT, so the field is never blank. A
  cited_only→external promotion preserves an existing non-empty value (prior edit).
- Enrich: ingest_precedent runs metadata extraction INLINE in-container
  (gemini_session is direct REST, no local CLI) with force_citation=True, upgrading
  the seed to the canonical derived citation (parties + reporter + date) before the
  chair opens the page. Best-effort: on no key / API failure the seed remains and
  the queued metadata drain stays as the fallback. Halacha extraction is untouched
  (stays local — claude_session).
- apply_to_record / extract_and_apply gain force_citation: re-assemble even when
  citation_formatted is non-empty, writing only when assembly SUCCEEDS (seed
  preserved on abstention). The drainer keeps the default (False) so a chair's
  manual edit in /precedents/[id] is never clobbered.

Ops: GOOGLE_GEMINI_API_KEY added to the legal-ai Coolify app (runtime) — the SoT
value from Infisical nautilus:/external-apis/gemini, validated against the API.

Invariants: G2 (one citation-resolution + one metadata-extraction path, reused —
no parallel logic), INV-ID2/X1§3 (citation_formatted stays a derived field; the
seed is provisional and upgraded), INV-G10 (chair edits preserved). No schema change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-20 15:30:00 +00:00
parent 2c515966c5
commit b0bcdbeeef
3 changed files with 54 additions and 8 deletions

View File

@@ -4334,12 +4334,19 @@ async def create_external_case_law(
precedent_level: str = "",
is_binding: bool = True,
document_id: UUID | None = None,
citation_formatted: str = "",
) -> dict:
"""Insert a chair-uploaded external precedent into case_law.
If a row with this ``case_number`` already exists with
source_kind='cited_only' (auto-discovered), promote it to
source_kind='external_upload' and fill in the missing fields.
``citation_formatted`` seeds the מראה-מקום from the chair's typed upload-form
value so the field is NEVER blank between upload and metadata extraction (the
inline enrichment later UPGRADES it to the canonical derived form via
``apply_to_record(force_citation=True)``). On a cited_only→external promotion
an existing non-empty value (a prior chair edit) is preserved.
"""
# INV-DM7: an appeals-committee source is persuasive even when uploaded via
# the external path — coerce to non-binding so it matches the committee
@@ -4364,11 +4371,12 @@ async def create_external_case_law(
summary, key_quote, full_text, source_url,
source_kind, document_id, extraction_status,
halacha_extraction_status, practice_area, appeal_subtype,
headnote, source_type, precedent_level, is_binding, content_hash
headnote, source_type, precedent_level, is_binding, content_hash,
citation_formatted
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9,
'external_upload', $10, 'processing', 'pending',
$11, $12, $13, $14, $15, $16, $17
$11, $12, $13, $14, $15, $16, $17, $18
)
ON CONFLICT (case_number) WHERE source_kind <> 'internal_committee'
DO UPDATE SET
@@ -4390,14 +4398,16 @@ async def create_external_case_law(
source_kind = 'external_upload',
extraction_status = 'processing',
halacha_extraction_status = 'pending',
content_hash = EXCLUDED.content_hash
content_hash = EXCLUDED.content_hash,
citation_formatted = COALESCE(
NULLIF(case_law.citation_formatted, ''), EXCLUDED.citation_formatted)
RETURNING *
""",
case_number, case_name, court, decision_date, tags_json,
summary, key_quote, full_text, source_url,
document_id, practice_area, appeal_subtype, headnote,
source_type, precedent_level, is_binding,
_content_hash(full_text),
_content_hash(full_text), (citation_formatted or "").strip(),
)
return _row_to_case_law(row)