"""Cross-reference system for intertextual awareness in translation. Provides biblical cross-references to help translators understand quotations, allusions, and thematic connections between passages. """ from __future__ import annotations import json from enum import Enum from pathlib import Path from typing import TYPE_CHECKING from pydantic import BaseModel, Field if TYPE_CHECKING: from src.models.verse import Verse, VerseReference class ReferenceType(str, Enum): """Type of cross-reference relationship.""" QUOTATION = "quotation" # Direct quote from another passage ALLUSION = "allusion" # Indirect reference or echo PARALLEL = "parallel" # Synoptic parallel (same event/teaching) THEMATIC = "thematic" # Thematic connection FULFILLMENT = "fulfillment" # Prophecy fulfillment TYPOLOGY = "typology" # Type/antitype relationship class CrossReference(BaseModel): """A cross-reference between two biblical passages.""" source_book: str = Field(..., description="Source book name") source_chapter: int = Field(..., ge=1) source_verse: int = Field(..., ge=1) target_book: str = Field(..., description="Target book name") target_chapter: int = Field(..., ge=1) target_verse_start: int = Field(..., ge=1) target_verse_end: int | None = Field(None, ge=1, description="End verse for ranges") reference_type: ReferenceType = Field( default=ReferenceType.THEMATIC, description="Type of cross-reference", ) note: str | None = Field(None, description="Explanation of the connection") translation_hint: str | None = Field( None, description="Suggestion for maintaining consistency with target passage", ) @property def source_ref(self) -> str: """Get source reference string.""" return f"{self.source_book} {self.source_chapter}:{self.source_verse}" @property def target_ref(self) -> str: """Get target reference string.""" if self.target_verse_end and self.target_verse_end != self.target_verse_start: return f"{self.target_book} {self.target_chapter}:{self.target_verse_start}-{self.target_verse_end}" return f"{self.target_book} {self.target_chapter}:{self.target_verse_start}" class CrossReferenceMatch(BaseModel): """A matched cross-reference with additional context.""" reference: CrossReference target_text: str | None = Field(None, description="Text of the target passage") target_translation: str | None = Field( None, description="Existing translation of target (if available)" ) relevance_score: float = Field( default=1.0, ge=0.0, le=1.0, description="Relevance score (1.0 = most relevant)", ) class CrossReferenceManager: """Manages biblical cross-references for translation context. Provides cross-reference lookup and formatting for inclusion in translation prompts to ensure consistency across related passages. """ def __init__(self, data_dir: Path | str | None = None) -> None: """Initialize the cross-reference manager. Args: data_dir: Path to data directory containing cross-reference files. """ self._references: list[CrossReference] = [] self._by_source: dict[str, list[CrossReference]] = {} self._by_target: dict[str, list[CrossReference]] = {} if data_dir: self._data_dir = Path(data_dir) self._load_cross_references() else: self._data_dir = None def _load_cross_references(self) -> None: """Load cross-references from data files.""" if not self._data_dir: return xref_path = self._data_dir / "metadata" / "cross_references.json" if not xref_path.exists(): return with xref_path.open("r", encoding="utf-8") as f: data = json.load(f) for entry in data.get("references", []): ref = CrossReference(**entry) self.add_reference(ref) def add_reference(self, ref: CrossReference) -> None: """Add a cross-reference to the index. Args: ref: The cross-reference to add. """ self._references.append(ref) # Index by source source_key = f"{ref.source_book}:{ref.source_chapter}:{ref.source_verse}" if source_key not in self._by_source: self._by_source[source_key] = [] self._by_source[source_key].append(ref) # Index by target target_key = f"{ref.target_book}:{ref.target_chapter}:{ref.target_verse_start}" if target_key not in self._by_target: self._by_target[target_key] = [] self._by_target[target_key].append(ref) def find_references_for_verse( self, book: str, chapter: int, verse: int, ) -> list[CrossReference]: """Find all cross-references for a given verse. Args: book: Book name. chapter: Chapter number. verse: Verse number. Returns: List of cross-references where this verse is the source. """ key = f"{book.lower()}:{chapter}:{verse}" return list(self._by_source.get(key, [])) def find_references_to_verse( self, book: str, chapter: int, verse: int, ) -> list[CrossReference]: """Find all references that point TO this verse. Args: book: Book name. chapter: Chapter number. verse: Verse number. Returns: List of cross-references where this verse is the target. """ key = f"{book.lower()}:{chapter}:{verse}" return list(self._by_target.get(key, [])) def find_references_by_type( self, book: str, chapter: int, verse: int, ref_type: ReferenceType, ) -> list[CrossReference]: """Find cross-references of a specific type for a verse. Args: book: Book name. chapter: Chapter number. verse: Verse number. ref_type: Type of reference to filter by. Returns: List of matching cross-references. """ refs = self.find_references_for_verse(book, chapter, verse) return [r for r in refs if r.reference_type == ref_type] def get_quotation_sources( self, book: str, chapter: int, verse: int, ) -> list[CrossReference]: """Get OT quotation sources for a verse. Particularly useful for NT verses that quote the OT. Args: book: Book name. chapter: Chapter number. verse: Verse number. Returns: List of quotation cross-references. """ return self.find_references_by_type( book, chapter, verse, ReferenceType.QUOTATION ) def get_parallel_passages( self, book: str, chapter: int, verse: int, ) -> list[CrossReference]: """Get synoptic parallel passages for a verse. Useful for ensuring consistency across Gospel parallels. Args: book: Book name. chapter: Chapter number. verse: Verse number. Returns: List of parallel passage cross-references. """ return self.find_references_by_type( book, chapter, verse, ReferenceType.PARALLEL ) def format_for_prompt( self, references: list[CrossReference], include_hints: bool = True, max_references: int = 5, ) -> str: """Format cross-references for inclusion in a translation prompt. Args: references: List of cross-references to format. include_hints: Whether to include translation hints. max_references: Maximum number of references to include. Returns: Formatted string for prompt inclusion. """ if not references: return "" lines = ["CROSS-REFERENCES:"] # Sort by relevance (quotations first, then type) type_priority = { ReferenceType.QUOTATION: 0, ReferenceType.FULFILLMENT: 1, ReferenceType.PARALLEL: 2, ReferenceType.ALLUSION: 3, ReferenceType.TYPOLOGY: 4, ReferenceType.THEMATIC: 5, } sorted_refs = sorted( references, key=lambda r: type_priority.get(r.reference_type, 99), ) for ref in sorted_refs[:max_references]: type_label = ref.reference_type.value.upper() lines.append(f"\n[{type_label}] {ref.target_ref}") if ref.note: lines.append(f" Note: {ref.note}") if include_hints and ref.translation_hint: lines.append(f" Translation hint: {ref.translation_hint}") return "\n".join(lines) def get_translation_consistency_notes( self, book: str, chapter: int, verse: int, ) -> list[str]: """Get notes about translation consistency for related passages. Args: book: Book name. chapter: Chapter number. verse: Verse number. Returns: List of consistency notes. """ notes = [] # Check for quotations quotations = self.get_quotation_sources(book, chapter, verse) if quotations: for q in quotations: notes.append( f"This verse quotes {q.target_ref}. " f"Consider maintaining consistency with the source passage." ) if q.translation_hint: notes.append(f"Hint: {q.translation_hint}") # Check for parallels parallels = self.get_parallel_passages(book, chapter, verse) if parallels: parallel_refs = [p.target_ref for p in parallels] notes.append( f"Synoptic parallels: {', '.join(parallel_refs)}. " f"Consider consistent phrasing for shared material." ) return notes def save(self, path: Path | None = None) -> None: """Save cross-references to file. Args: path: Path to save to. Uses default if not provided. """ if path is None: if self._data_dir is None: raise ValueError("No data directory configured") path = self._data_dir / "metadata" / "cross_references.json" path.parent.mkdir(parents=True, exist_ok=True) data = { "version": "1.0", "references": [ref.model_dump() for ref in self._references], } with path.open("w", encoding="utf-8") as f: json.dump(data, f, indent=2, ensure_ascii=False) def __len__(self) -> int: """Get the number of cross-references.""" return len(self._references) # Well-known cross-references for John's Gospel (examples) JOHN_CROSS_REFERENCES = [ CrossReference( source_book="john", source_chapter=1, source_verse=1, target_book="genesis", target_chapter=1, target_verse_start=1, reference_type=ReferenceType.ALLUSION, note="John's opening 'In the beginning' (Ἐν ἀρχῇ) deliberately echoes Genesis", translation_hint="Consider using similar phrasing to Genesis 1:1 (初めに)", ), CrossReference( source_book="john", source_chapter=1, source_verse=14, target_book="exodus", target_chapter=33, target_verse_start=7, target_verse_end=11, reference_type=ReferenceType.TYPOLOGY, note="'Tabernacled among us' (ἐσκήνωσεν) echoes the tabernacle/tent of meeting", translation_hint="The word 'dwelt' has tabernacle imagery", ), CrossReference( source_book="john", source_chapter=1, source_verse=23, target_book="isaiah", target_chapter=40, target_verse_start=3, reference_type=ReferenceType.QUOTATION, note="John the Baptist quotes Isaiah 40:3", translation_hint="Match LXX/Isaiah translation for 'voice crying in the wilderness'", ), CrossReference( source_book="john", source_chapter=1, source_verse=29, target_book="exodus", target_chapter=12, target_verse_start=1, target_verse_end=13, reference_type=ReferenceType.TYPOLOGY, note="'Lamb of God' connects to Passover lamb", translation_hint="神の小羊 - maintain lamb imagery consistent with Passover", ), CrossReference( source_book="john", source_chapter=3, source_verse=14, target_book="numbers", target_chapter=21, target_verse_start=8, target_verse_end=9, reference_type=ReferenceType.TYPOLOGY, note="Bronze serpent lifted up prefigures Christ's crucifixion", translation_hint="Use 上げる for 'lifted up' consistently", ), ] def create_default_cross_references(data_dir: Path) -> CrossReferenceManager: """Create a CrossReferenceManager with default NT cross-references. Args: data_dir: Data directory path. Returns: CrossReferenceManager with pre-loaded references. """ manager = CrossReferenceManager(data_dir) # Add well-known references if not already loaded if len(manager) == 0: for ref in JOHN_CROSS_REFERENCES: manager.add_reference(ref) return manager