# #region DictionaryCorrectionService [C:3] [TYPE Class] [SEMANTICS dictionary, correction, bulk] # @BRIEF Submit term corrections and bulk corrections to dictionaries with conflict detection. # @RELATION DEPENDS_ON -> [DictionaryEntry] # @RELATION DEPENDS_ON -> [TerminologyDictionary] from typing import Any from sqlalchemy import or_ from sqlalchemy.orm import Session from ...core.logger import belief_scope, logger from ...models.translate import DictionaryEntry, TerminologyDictionary from ._utils import _normalize_term class DictionaryCorrectionService: """Submit term corrections to dictionaries with conflict detection and bulk support.""" # region submit_correction [TYPE Function] # @PURPOSE: Submit a term correction from a run result. @staticmethod def submit_correction( db: Session, dict_id: str, source_term: str, incorrect_target_term: str, corrected_target_term: str, origin_run_id: str | None = None, origin_row_key: str | None = None, origin_user_id: str | None = None, on_conflict: str = "overwrite", context_data: dict[str, Any] | None = None, usage_notes: str | None = None, keep_context: bool = True, ) -> dict[str, Any]: with belief_scope("DictionaryCorrectionService.submit_correction"): dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first() if not dictionary: raise ValueError(f"Dictionary '{dict_id}' not found") effective_context = context_data effective_context_source = "auto" if not keep_context: effective_context = None effective_context_source = "manual" normalized = _normalize_term(source_term) entry_src_lang = "und" entry_tgt_lang = "und" existing = ( db.query(DictionaryEntry) .filter( DictionaryEntry.dictionary_id == dict_id, DictionaryEntry.source_term_normalized == normalized, or_( (DictionaryEntry.source_language == entry_src_lang) & (DictionaryEntry.target_language == entry_tgt_lang), (DictionaryEntry.source_language == "und") & (DictionaryEntry.target_language == "und"), ), ) .first() ) result: dict[str, Any] = {"source_term": source_term, "target_term": corrected_target_term, "action": "created", "entry_id": None, "conflict": None, "message": None} if existing: if on_conflict == "keep_existing": result["action"] = "conflict_detected" result["conflict"] = {"source_term": source_term, "existing_target_term": existing.target_term, "submitted_target_term": corrected_target_term, "action": "keep_existing"} result["message"] = f"Existing entry kept: '{existing.target_term}'" return result elif on_conflict == "overwrite": existing.target_term = corrected_target_term.strip() if origin_run_id: existing.origin_run_id = origin_run_id if origin_row_key: existing.origin_row_key = origin_row_key if origin_user_id: existing.origin_user_id = origin_user_id if context_data is not None or not keep_context: existing.context_data = effective_context existing.has_context = bool(effective_context) existing.context_source = effective_context_source if usage_notes is not None: existing.usage_notes = usage_notes db.flush() result["action"] = "updated" result["entry_id"] = existing.id result["message"] = f"Entry updated from '{existing.target_term}' to '{corrected_target_term}'" else: result["action"] = "skipped" result["conflict"] = {"source_term": source_term, "existing_target_term": existing.target_term, "submitted_target_term": corrected_target_term, "action": "cancel"} result["message"] = "Correction cancelled by conflict mode" return result else: entry = DictionaryEntry( dictionary_id=dict_id, source_term=source_term.strip(), source_term_normalized=normalized, target_term=corrected_target_term.strip(), source_language=entry_src_lang, target_language=entry_tgt_lang, context_data=effective_context, usage_notes=usage_notes, has_context=bool(effective_context), context_source=effective_context_source if effective_context else None, origin_run_id=origin_run_id, origin_row_key=origin_row_key, origin_user_id=origin_user_id, ) db.add(entry) db.flush() result["entry_id"] = entry.id result["message"] = f"Entry created for '{source_term}' -> '{corrected_target_term}'" db.commit() logger.reflect("Correction processed", result) return result # endregion submit_correction # region submit_bulk_corrections [TYPE Function] # @PURPOSE: Submit multiple term corrections atomically. @staticmethod def submit_bulk_corrections( db: Session, dict_id: str, corrections: list[dict[str, Any]], origin_user_id: str | None = None, ) -> dict[str, Any]: with belief_scope("DictionaryCorrectionService.submit_bulk_corrections"): dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first() if not dictionary: raise ValueError(f"Dictionary '{dict_id}' not found") results: list[dict[str, Any]] = [] conflicts: list[dict[str, Any]] = [] all_ok = True for corr in corrections: source_term = corr.get("source_term", "").strip() corrected_target = corr.get("corrected_target_term", "").strip() if not source_term or not corrected_target: results.append({"source_term": source_term, "action": "error", "message": "source_term and corrected_target_term are required"}) all_ok = False continue normalized = _normalize_term(source_term) bulk_src_lang = "und" bulk_tgt_lang = "und" existing = ( db.query(DictionaryEntry) .filter( DictionaryEntry.dictionary_id == dict_id, DictionaryEntry.source_term_normalized == normalized, or_( (DictionaryEntry.source_language == bulk_src_lang) & (DictionaryEntry.target_language == bulk_tgt_lang), (DictionaryEntry.source_language == "und") & (DictionaryEntry.target_language == "und"), ), ) .first() ) if existing: on_conflict = corr.get("on_conflict", "overwrite") if on_conflict == "keep_existing": conflicts.append({"source_term": source_term, "existing_target_term": existing.target_term, "submitted_target_term": corrected_target, "action": "keep_existing"}) results.append({"source_term": source_term, "action": "conflict_detected", "message": f"Existing entry kept: '{existing.target_term}'"}) continue elif on_conflict == "overwrite": existing.target_term = corrected_target existing.origin_user_id = origin_user_id results.append({"source_term": source_term, "action": "updated", "entry_id": existing.id, "message": f"Updated to '{corrected_target}'"}) else: conflicts.append({"source_term": source_term, "existing_target_term": existing.target_term, "submitted_target_term": corrected_target, "action": "cancel"}) results.append({"source_term": source_term, "action": "skipped", "message": "Cancelled by conflict mode"}) else: entry = DictionaryEntry( dictionary_id=dict_id, source_term=source_term, source_term_normalized=normalized, target_term=corrected_target, source_language=bulk_src_lang, target_language=bulk_tgt_lang, origin_run_id=corr.get("origin_run_id"), origin_row_key=corr.get("origin_row_key"), origin_user_id=origin_user_id, ) db.add(entry) results.append({"source_term": source_term, "action": "created", "message": f"Entry created for '{source_term}' -> '{corrected_target}'"}) if conflicts and not all_ok: db.rollback() return {"status": "conflicts", "results": results, "conflicts": conflicts, "message": "Conflicts detected. Resolve before retrying."} db.commit() return { "status": "completed", "results": results, "conflicts": conflicts, "total": len(corrections), "applied": sum(1 for r in results if r["action"] in ("created", "updated")), } # endregion submit_bulk_corrections # #endregion DictionaryCorrectionService