OPEN SOURCE
software/src/orchestration/provenance.py
Read on this site and download the files you need.
Nichijoyaku was created using AI. A complete review by human biblical-language and biblical-studies specialists has not been established. Read alongside established translations and original-language resources.
← Back to the open-source guide
1"""Provenance tracking for translation audit trails.23Captures comprehensive input information for every translation,4enabling full reproducibility and audit capability.5"""67from __future__ import annotations89import hashlib10from dataclasses import dataclass, field11from datetime import datetime12from typing import Any131415def compute_hash(text: str | None) -> str:16"""Compute SHA256 hash of text.1718Args:19text: Text to hash, or None.2021Returns:22Hex-encoded SHA256 hash, or empty string if text is None.23"""24if not text:25return ""26return hashlib.sha256(text.encode("utf-8")).hexdigest()272829@dataclass30class TranslationProvenance:31"""Complete provenance record for a single translation.3233Tracks all inputs that contributed to a translation, enabling:34- Full audit trails35- Reproducibility verification36- Change detection (via content hashes)37- Debugging and quality investigation3839Attributes:40source_texts: Source texts used (keyed by type: greek, hebrew, kjv, etc.).41source_hashes: SHA256 hashes of source texts for change detection.42preceding_verses: List of verse refs used as preceding context.43following_verses: List of verse refs used as following context.44pericope_id: ID of the pericope this verse belongs to, if any.45style_config_hash: Hash of the style configuration used.46glossary_hash: Hash of glossary terms used.47prompt_version: Version identifier for the prompt template.48model: Model used for translation.49system_prompt: Complete system prompt sent to Claude.50user_prompt: Complete user prompt sent to Claude.51tm_matches_used: Translation memory matches used.52attempt_number: Which attempt this was (1 = first try).53started_at: When translation started.54completed_at: When translation completed.55evaluation_score: Quality score from evaluation (if evaluated).56evaluation_flags: Quality flags from evaluation.57chapter_summary: Chapter summary used as context.58book_metadata: Book metadata used as context.59"""6061# Source inputs with hashes for change detection62source_texts: dict[str, str] = field(default_factory=dict)63source_hashes: dict[str, str] = field(default_factory=dict)6465# Context inputs66preceding_verses: list[str] = field(default_factory=list)67following_verses: list[str] = field(default_factory=list)68pericope_id: str | None = None6970# Configuration (frozen at translation time)71style_config_hash: str = ""72glossary_hash: str = ""73prompt_version: str = "v1.0"74model: str = ""7576# Full prompts (complete audit trail)77system_prompt: str = ""78user_prompt: str = ""7980# Translation memory inputs81tm_matches_used: list[dict[str, Any]] = field(default_factory=list)8283# Execution details84attempt_number: int = 185started_at: datetime | None = None86completed_at: datetime | None = None8788# Quality89evaluation_score: float | None = None90evaluation_flags: list[str] = field(default_factory=list)9192# Additional context93chapter_summary: str | None = None94book_metadata: dict[str, Any] = field(default_factory=dict)9596def compute_source_hashes(self) -> None:97"""Compute hashes for all source texts."""98self.source_hashes = {99key: compute_hash(text) for key, text in self.source_texts.items()100}101102def to_dict(self) -> dict[str, Any]:103"""Convert provenance to a dictionary for JSON serialization.104105Returns:106Dictionary representation of provenance.107"""108return {109"source_texts": self.source_texts,110"source_hashes": self.source_hashes,111"preceding_verses": self.preceding_verses,112"following_verses": self.following_verses,113"pericope_id": self.pericope_id,114"style_config_hash": self.style_config_hash,115"glossary_hash": self.glossary_hash,116"prompt_version": self.prompt_version,117"model": self.model,118"system_prompt": self.system_prompt,119"user_prompt": self.user_prompt,120"tm_matches_used": self.tm_matches_used,121"attempt_number": self.attempt_number,122"started_at": self.started_at.isoformat() if self.started_at else None,123"completed_at": self.completed_at.isoformat() if self.completed_at else None,124"evaluation_score": self.evaluation_score,125"evaluation_flags": self.evaluation_flags,126"chapter_summary": self.chapter_summary,127"book_metadata": self.book_metadata,128}129130@classmethod131def from_dict(cls, data: dict[str, Any]) -> TranslationProvenance:132"""Create provenance from a dictionary.133134Args:135data: Dictionary representation.136137Returns:138TranslationProvenance instance.139"""140return cls(141source_texts=data.get("source_texts", {}),142source_hashes=data.get("source_hashes", {}),143preceding_verses=data.get("preceding_verses", []),144following_verses=data.get("following_verses", []),145pericope_id=data.get("pericope_id"),146style_config_hash=data.get("style_config_hash", ""),147glossary_hash=data.get("glossary_hash", ""),148prompt_version=data.get("prompt_version", "v1.0"),149model=data.get("model", ""),150system_prompt=data.get("system_prompt", ""),151user_prompt=data.get("user_prompt", ""),152tm_matches_used=data.get("tm_matches_used", []),153attempt_number=data.get("attempt_number", 1),154started_at=(155datetime.fromisoformat(data["started_at"])156if data.get("started_at")157else None158),159completed_at=(160datetime.fromisoformat(data["completed_at"])161if data.get("completed_at")162else None163),164evaluation_score=data.get("evaluation_score"),165evaluation_flags=data.get("evaluation_flags", []),166chapter_summary=data.get("chapter_summary"),167book_metadata=data.get("book_metadata", {}),168)169170def has_source_change(self, other: TranslationProvenance) -> bool:171"""Check if source texts have changed from another provenance.172173Args:174other: Another provenance to compare against.175176Returns:177True if any source hashes differ.178"""179for key, hash_value in self.source_hashes.items():180if other.source_hashes.get(key) != hash_value:181return True182return False183184def get_inputs_summary(self) -> dict[str, Any]:185"""Get a summary of inputs for logging/display.186187Returns:188Dictionary with input summary.189"""190return {191"sources": list(self.source_texts.keys()),192"preceding_context": len(self.preceding_verses),193"following_context": len(self.following_verses),194"pericope": self.pericope_id is not None,195"tm_matches": len(self.tm_matches_used),196"model": self.model,197"attempt": self.attempt_number,198}199200201def compute_glossary_hash(terms: list[Any]) -> str:202"""Compute hash of glossary terms used.203204Args:205terms: List of glossary terms (any type with string representation).206207Returns:208SHA256 hash of sorted term representations.209"""210if not terms:211return ""212# Sort by string representation for consistency213term_strs = sorted(str(t) for t in terms)214combined = "|".join(term_strs)215return compute_hash(combined)216217218def compute_style_hash(style_config: dict[str, Any]) -> str:219"""Compute hash of style configuration.220221Args:222style_config: Style configuration dictionary.223224Returns:225SHA256 hash of JSON-serialized config.226"""227if not style_config:228return ""229import json230231# Sort keys for consistency232json_str = json.dumps(style_config, sort_keys=True)233return compute_hash(json_str)234