Spaces:
Running
Running
R14-TQ1-R4-R1: evidence-carrying semantic repair and durable claim recovery
#51
by GuXiaopeng - opened
- core/generation_runner.py +30 -14
- core/pipeline_agent.py +167 -18
core/generation_runner.py
CHANGED
|
@@ -871,9 +871,9 @@ class GenerationRunner:
|
|
| 871 |
discovery cannot see them. No semantic synonym merging is performed.
|
| 872 |
"""
|
| 873 |
items = []
|
| 874 |
-
|
| 875 |
|
| 876 |
-
def add(value, kind):
|
| 877 |
term = re.sub(r'\s+', ' ', str(value or '')).strip(' \t\r\n,;:.')
|
| 878 |
if not term or len(term) < 3 or len(term) > 100:
|
| 879 |
return
|
|
@@ -881,32 +881,37 @@ class GenerationRunner:
|
|
| 881 |
if not words or len(words) > 10:
|
| 882 |
return
|
| 883 |
key = re.sub(r'[–—-]+', '-', term).casefold()
|
| 884 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 885 |
return
|
| 886 |
-
|
| 887 |
-
items.append({'source': term, 'kind': str(kind or 'concept')})
|
| 888 |
|
| 889 |
meta = dict(data_metadata or {})
|
| 890 |
if str(mode or '') == 'amj':
|
| 891 |
constructs = meta.get('constructs') if isinstance(meta.get('constructs'), dict) else {}
|
| 892 |
for role in ('predictor', 'mediator', 'outcome'):
|
| 893 |
-
add(constructs.get(role), f'quantitative_{role}')
|
| 894 |
else:
|
| 895 |
for theme in (meta.get('themes') or []):
|
| 896 |
if not isinstance(theme, dict):
|
| 897 |
continue
|
| 898 |
-
add(theme.get('title'), 'qualitative_theme')
|
| 899 |
for concept in (theme.get('concepts') or []):
|
| 900 |
-
add(concept, 'qualitative_concept')
|
| 901 |
for row in (meta.get('evidence') or []):
|
| 902 |
if isinstance(row, dict):
|
| 903 |
-
add(row.get('concept'), 'qualitative_concept')
|
| 904 |
|
| 905 |
for task in (adaptive_structure or []):
|
| 906 |
if not isinstance(task, dict):
|
| 907 |
continue
|
| 908 |
for concept in (task.get('concepts') or []):
|
| 909 |
-
add(concept, 'outline_concept')
|
| 910 |
|
| 911 |
# User keywords remain planning seeds, not canonical identities. Use
|
| 912 |
# them only as a fallback when frozen empirical/qualitative metadata and
|
|
@@ -915,7 +920,7 @@ class GenerationRunner:
|
|
| 915 |
for seed in re.split(r'[,;\n|]+', str(keywords or '')):
|
| 916 |
seed = re.sub(r'\s+', ' ', seed).strip()
|
| 917 |
if 1 <= len(seed.split()) <= 6:
|
| 918 |
-
add(seed, 'keyword_fallback')
|
| 919 |
|
| 920 |
# Capture explicit lower-case hyphenated academic expressions only when
|
| 921 |
# they recur or carry a research-concept cue. This closes the
|
|
@@ -932,9 +937,9 @@ class GenerationRunner:
|
|
| 932 |
for term in hyphenated:
|
| 933 |
parts = set(term.casefold().split('-'))
|
| 934 |
if counts.get(term.casefold(), 0) >= 2 or parts.intersection(concept_cues):
|
| 935 |
-
add(term, 'logic_hyphenated_concept')
|
| 936 |
|
| 937 |
-
return {'version': 'concept-ledger-
|
| 938 |
|
| 939 |
@staticmethod
|
| 940 |
def _canonical_concept_writer_directive(ledger: dict) -> str:
|
|
@@ -1270,15 +1275,22 @@ class GenerationRunner:
|
|
| 1270 |
'glossary': translation_glossary, 'section_name': unit_name, 'max_attempts': 2,
|
| 1271 |
}
|
| 1272 |
try:
|
| 1273 |
-
parameters = inspect.signature(translate_method).parameters.values()
|
| 1274 |
supports_stage_callback = any(
|
| 1275 |
item.name == 'stage_callback' or item.kind == inspect.Parameter.VAR_KEYWORD
|
| 1276 |
for item in parameters
|
| 1277 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1278 |
except (TypeError, ValueError):
|
| 1279 |
supports_stage_callback = False
|
|
|
|
| 1280 |
if supports_stage_callback:
|
| 1281 |
call_kwargs['stage_callback'] = publish_translation_stage
|
|
|
|
|
|
|
| 1282 |
result = translate_method(translation_source, target_lang, **call_kwargs)
|
| 1283 |
translated_text = str(result.get('text') or '').strip()
|
| 1284 |
if pending_quote_tags or reused_quote_tags:
|
|
@@ -1314,6 +1326,8 @@ class GenerationRunner:
|
|
| 1314 |
'fact_ledger_hash': fact_ledger_hash,
|
| 1315 |
'translated_text': '',
|
| 1316 |
'qa_status': 'repair_pending',
|
|
|
|
|
|
|
| 1317 |
'error_code': error_code,
|
| 1318 |
'attempts': int(existing.get('attempts') or 0) + 1,
|
| 1319 |
'deferred_at': datetime.now(timezone.utc).isoformat(),
|
|
@@ -1344,6 +1358,8 @@ class GenerationRunner:
|
|
| 1344 |
'qa_status': str(result.get('qa_status') or 'passed'),
|
| 1345 |
'language_adapter': str(result.get('language_adapter') or ''),
|
| 1346 |
'diagnostics': list(result.get('diagnostics') or [])[-20:],
|
|
|
|
|
|
|
| 1347 |
'attempts': int(result.get('attempts') or 1),
|
| 1348 |
'completed_at': datetime.now(timezone.utc).isoformat(),
|
| 1349 |
}
|
|
|
|
| 871 |
discovery cannot see them. No semantic synonym merging is performed.
|
| 872 |
"""
|
| 873 |
items = []
|
| 874 |
+
index_by_key = {}
|
| 875 |
|
| 876 |
+
def add(value, kind, policy='FLEXIBLE_TERM'):
|
| 877 |
term = re.sub(r'\s+', ' ', str(value or '')).strip(' \t\r\n,;:.')
|
| 878 |
if not term or len(term) < 3 or len(term) > 100:
|
| 879 |
return
|
|
|
|
| 881 |
if not words or len(words) > 10:
|
| 882 |
return
|
| 883 |
key = re.sub(r'[–—-]+', '-', term).casefold()
|
| 884 |
+
normalized_policy = 'STRICT_CONSTRUCT' if str(policy or '').upper() == 'STRICT_CONSTRUCT' else 'FLEXIBLE_TERM'
|
| 885 |
+
if key in index_by_key:
|
| 886 |
+
row = items[index_by_key[key]]
|
| 887 |
+
if normalized_policy == 'STRICT_CONSTRUCT' and row.get('policy') != 'STRICT_CONSTRUCT':
|
| 888 |
+
row['policy'] = 'STRICT_CONSTRUCT'
|
| 889 |
+
row['kind'] = str(kind or row.get('kind') or 'concept')
|
| 890 |
return
|
| 891 |
+
index_by_key[key] = len(items)
|
| 892 |
+
items.append({'source': term, 'kind': str(kind or 'concept'), 'policy': normalized_policy})
|
| 893 |
|
| 894 |
meta = dict(data_metadata or {})
|
| 895 |
if str(mode or '') == 'amj':
|
| 896 |
constructs = meta.get('constructs') if isinstance(meta.get('constructs'), dict) else {}
|
| 897 |
for role in ('predictor', 'mediator', 'outcome'):
|
| 898 |
+
add(constructs.get(role), f'quantitative_{role}', 'STRICT_CONSTRUCT')
|
| 899 |
else:
|
| 900 |
for theme in (meta.get('themes') or []):
|
| 901 |
if not isinstance(theme, dict):
|
| 902 |
continue
|
| 903 |
+
add(theme.get('title'), 'qualitative_theme', 'STRICT_CONSTRUCT')
|
| 904 |
for concept in (theme.get('concepts') or []):
|
| 905 |
+
add(concept, 'qualitative_concept', 'STRICT_CONSTRUCT')
|
| 906 |
for row in (meta.get('evidence') or []):
|
| 907 |
if isinstance(row, dict):
|
| 908 |
+
add(row.get('concept'), 'qualitative_concept', 'STRICT_CONSTRUCT')
|
| 909 |
|
| 910 |
for task in (adaptive_structure or []):
|
| 911 |
if not isinstance(task, dict):
|
| 912 |
continue
|
| 913 |
for concept in (task.get('concepts') or []):
|
| 914 |
+
add(concept, 'outline_concept', 'FLEXIBLE_TERM')
|
| 915 |
|
| 916 |
# User keywords remain planning seeds, not canonical identities. Use
|
| 917 |
# them only as a fallback when frozen empirical/qualitative metadata and
|
|
|
|
| 920 |
for seed in re.split(r'[,;\n|]+', str(keywords or '')):
|
| 921 |
seed = re.sub(r'\s+', ' ', seed).strip()
|
| 922 |
if 1 <= len(seed.split()) <= 6:
|
| 923 |
+
add(seed, 'keyword_fallback', 'FLEXIBLE_TERM')
|
| 924 |
|
| 925 |
# Capture explicit lower-case hyphenated academic expressions only when
|
| 926 |
# they recur or carry a research-concept cue. This closes the
|
|
|
|
| 937 |
for term in hyphenated:
|
| 938 |
parts = set(term.casefold().split('-'))
|
| 939 |
if counts.get(term.casefold(), 0) >= 2 or parts.intersection(concept_cues):
|
| 940 |
+
add(term, 'logic_hyphenated_concept', 'FLEXIBLE_TERM')
|
| 941 |
|
| 942 |
+
return {'version': 'concept-ledger-v2', 'items': items}
|
| 943 |
|
| 944 |
@staticmethod
|
| 945 |
def _canonical_concept_writer_directive(ledger: dict) -> str:
|
|
|
|
| 1275 |
'glossary': translation_glossary, 'section_name': unit_name, 'max_attempts': 2,
|
| 1276 |
}
|
| 1277 |
try:
|
| 1278 |
+
parameters = list(inspect.signature(translate_method).parameters.values())
|
| 1279 |
supports_stage_callback = any(
|
| 1280 |
item.name == 'stage_callback' or item.kind == inspect.Parameter.VAR_KEYWORD
|
| 1281 |
for item in parameters
|
| 1282 |
)
|
| 1283 |
+
supports_claim_states = any(
|
| 1284 |
+
item.name == 'claim_states' or item.kind == inspect.Parameter.VAR_KEYWORD
|
| 1285 |
+
for item in parameters
|
| 1286 |
+
)
|
| 1287 |
except (TypeError, ValueError):
|
| 1288 |
supports_stage_callback = False
|
| 1289 |
+
supports_claim_states = False
|
| 1290 |
if supports_stage_callback:
|
| 1291 |
call_kwargs['stage_callback'] = publish_translation_stage
|
| 1292 |
+
if supports_claim_states and isinstance(existing.get('claim_states'), dict):
|
| 1293 |
+
call_kwargs['claim_states'] = dict(existing.get('claim_states') or {})
|
| 1294 |
result = translate_method(translation_source, target_lang, **call_kwargs)
|
| 1295 |
translated_text = str(result.get('text') or '').strip()
|
| 1296 |
if pending_quote_tags or reused_quote_tags:
|
|
|
|
| 1326 |
'fact_ledger_hash': fact_ledger_hash,
|
| 1327 |
'translated_text': '',
|
| 1328 |
'qa_status': 'repair_pending',
|
| 1329 |
+
'claim_states': dict(getattr(agent, '_last_translation_claim_states', {}) or {}),
|
| 1330 |
+
'claim_state_version': 'translation-claim-state-v1',
|
| 1331 |
'error_code': error_code,
|
| 1332 |
'attempts': int(existing.get('attempts') or 0) + 1,
|
| 1333 |
'deferred_at': datetime.now(timezone.utc).isoformat(),
|
|
|
|
| 1358 |
'qa_status': str(result.get('qa_status') or 'passed'),
|
| 1359 |
'language_adapter': str(result.get('language_adapter') or ''),
|
| 1360 |
'diagnostics': list(result.get('diagnostics') or [])[-20:],
|
| 1361 |
+
'claim_states': dict(result.get('claim_states') or {}),
|
| 1362 |
+
'claim_state_version': str(result.get('claim_state_version') or 'translation-claim-state-v1'),
|
| 1363 |
'attempts': int(result.get('attempts') or 1),
|
| 1364 |
'completed_at': datetime.now(timezone.utc).isoformat(),
|
| 1365 |
}
|
core/pipeline_agent.py
CHANGED
|
@@ -4767,13 +4767,26 @@ Output only the repaired section."""
|
|
| 4767 |
mapping = dict(existing or {})
|
| 4768 |
rows = (dict(concept_ledger or {}).get('items') or []) if isinstance(concept_ledger, dict) else []
|
| 4769 |
sources = []
|
|
|
|
|
|
|
| 4770 |
for row in rows:
|
| 4771 |
if not isinstance(row, dict):
|
| 4772 |
continue
|
| 4773 |
term = re.sub(r'\s+', ' ', str(row.get('source') or '')).strip()
|
| 4774 |
-
if term
|
| 4775 |
-
|
| 4776 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4777 |
if not sources:
|
| 4778 |
return mapping
|
| 4779 |
|
|
@@ -5002,6 +5015,14 @@ SECTION: {section_name}
|
|
| 5002 |
'UNCERTAINTY_DRIFT','TERMINOLOGY','GRAMMAR','LANGUAGE_CONTAMINATION',
|
| 5003 |
'CITATION_ATTACHMENT','ACRONYM_ATTACHMENT','SEMANTIC_DRIFT',
|
| 5004 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5005 |
claim_rows = []
|
| 5006 |
glossary_value = dict(glossary or {})
|
| 5007 |
for block_id in expected_ids:
|
|
@@ -5040,8 +5061,8 @@ SECTION: {section_name}
|
|
| 5040 |
Review EACH claim independently. Do not rewrite and do not provide free-form explanations. Check semantic equivalence and language quality: omissions/additions, negation, uncertainty, association-versus-causation strength, terminology, grammar, language contamination, citation attachment, and acronym attachment.
|
| 5041 |
LANGUAGE ADAPTER: {profile['adapter_id']}; morphology={profile['morphology']}. {profile['term_policy']} {profile['contamination_policy']}
|
| 5042 |
For Russian, do not flag a terminology error merely because a translated term is inflected for grammatical case/number/gender. Russian LANGUAGE_CONTAMINATION includes sustained unprotected English prose and ordinary untranslated English lexemes; never use it for protected acronyms, AI/LLM/SEM, model or tool names, variable names, citations, canonical glossary terms, or source-matched proper names/titles. For Chinese, ordinary untranslated English lexical words are contamination while protected technical objects remain legal. For Kazakh, do not flag a terminology error merely because productive suffixes attach to the correct term stem. Kazakh LANGUAGE_CONTAMINATION includes ordinary untranslated English lexemes and clearly Russian lexical/syntactic intrusions not justified by shared international terminology; Cyrillic script or absence of Kazakh-specific letters alone is never sufficient evidence.
|
| 5043 |
-
Return valid JSON only in this exact form: {{"items":[{{"id":"SP-P000001-C000001","ok":true,"codes":[]}}]}}.
|
| 5044 |
-
If ok=false, codes MUST contain only values from allowed_issue_codes. Never return numbered prose, examples, quotes, or corrective wording. Return every supplied id exactly once."""
|
| 5045 |
response = self.call_deepseek(
|
| 5046 |
system_prompt, json.dumps(payload, ensure_ascii=False),
|
| 5047 |
max_tokens=max(1200, min(3600, 650 + 260 * len(rows))),
|
|
@@ -5062,6 +5083,29 @@ If ok=false, codes MUST contain only values from allowed_issue_codes. Never retu
|
|
| 5062 |
meta = row_lookup[row_id]
|
| 5063 |
raw_codes = [str(code or '').strip().upper() for code in (returned_row.get('codes') or [])]
|
| 5064 |
codes = [code for code in raw_codes if code in allowed_codes]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5065 |
model_contamination = 'LANGUAGE_CONTAMINATION' in codes
|
| 5066 |
lexical = self._translation_lexical_purity_evidence(
|
| 5067 |
meta['english'], meta['target'], target_lang, relevant_glossary
|
|
@@ -5473,12 +5517,19 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
|
|
| 5473 |
]
|
| 5474 |
if lexical_tokens:
|
| 5475 |
evidence_payload['detected_unprotected_english_tokens'] = lexical_tokens[:8]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5476 |
effective_prompt = system_prompt + (
|
| 5477 |
'\nCLAIM QUALITY RETRY. Repair only this one claim. The only permitted repair signals are the following bounded issue codes: '
|
| 5478 |
+ json.dumps(codes[:5], ensure_ascii=False)
|
| 5479 |
+ '. Use the bounded detector evidence below only to repair the supplied claim; do not invent additional defects. '
|
| 5480 |
'The current target claim is context, not authority: the frozen English source remains authoritative. '
|
| 5481 |
'If detected_unprotected_english_tokens is non-empty, every listed token is deterministic lexical-purity evidence and MUST be translated or removed from ordinary target-language prose; do not preserve it unless it is represented by a protected SP-T anchor. '
|
|
|
|
| 5482 |
'Preserve every SP-P, SP-C, and SP-T marker byte-for-byte. Do not add claims or factual content. Return only the marked translated claim.'
|
| 5483 |
+ '\n<CLAIM_REPAIR_EVIDENCE>\n'
|
| 5484 |
+ json.dumps(evidence_payload, ensure_ascii=False)
|
|
@@ -5553,6 +5604,65 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
|
|
| 5553 |
_order, segments = self._parse_translation_claim_segments(restored, [claim_id])
|
| 5554 |
return segments[claim_id]
|
| 5555 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5556 |
def _adjudicate_persistent_translation_issues(
|
| 5557 |
self, sources, translations, issues, target_lang, glossary=None, section_name=''
|
| 5558 |
):
|
|
@@ -5740,7 +5850,7 @@ Decision must be CONFIRMED_BLOCKING or NOT_CONFIRMED. evidence_code must be one
|
|
| 5740 |
)
|
| 5741 |
return {}
|
| 5742 |
|
| 5743 |
-
def translate_frozen_section(self, section_text, target_lang, glossary=None, section_name='', max_attempts=2, stage_callback=None):
|
| 5744 |
"""Translate one frozen English unit and publish only after all QA passes."""
|
| 5745 |
value = str(section_text or '').strip()
|
| 5746 |
if not value:
|
|
@@ -5769,8 +5879,15 @@ Decision must be CONFIRMED_BLOCKING or NOT_CONFIRMED. evidence_code must be one
|
|
| 5769 |
+ '\nUse this frozen terminology glossary consistently. Never remap an existing term:\n'
|
| 5770 |
+ json.dumps(frozen_glossary, ensure_ascii=False)
|
| 5771 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5772 |
attempts = max(1, int(max_attempts or 1))
|
| 5773 |
last_error = None
|
|
|
|
|
|
|
| 5774 |
|
| 5775 |
def emit_stage(stage):
|
| 5776 |
if not callable(stage_callback):
|
|
@@ -5788,17 +5905,33 @@ Decision must be CONFIRMED_BLOCKING or NOT_CONFIRMED. evidence_code must be one
|
|
| 5788 |
emit_stage('translating')
|
| 5789 |
translations = {}
|
| 5790 |
try:
|
| 5791 |
-
|
| 5792 |
-
|
| 5793 |
-
|
| 5794 |
-
|
| 5795 |
-
for
|
| 5796 |
-
|
| 5797 |
-
|
| 5798 |
-
|
| 5799 |
-
|
| 5800 |
-
|
| 5801 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5802 |
)
|
| 5803 |
local_round = 0
|
| 5804 |
while quality_issues and local_round < 2:
|
|
@@ -5829,7 +5962,15 @@ Decision must be CONFIRMED_BLOCKING or NOT_CONFIRMED. evidence_code must be one
|
|
| 5829 |
claim_order = list(source_claims)
|
| 5830 |
_order, segments = self._parse_translation_claim_segments(translations[block_id], claim_order)
|
| 5831 |
current_target_claim = segments[claim_id]
|
| 5832 |
-
repair_evidence =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5833 |
if 'LANGUAGE_CONTAMINATION' in {str(code or '').strip().upper() for code in (codes or [])}:
|
| 5834 |
lexical = self._translation_lexical_purity_evidence(
|
| 5835 |
source_claims[claim_id], current_target_claim, target_lang, frozen_glossary
|
|
@@ -5871,12 +6012,18 @@ Decision must be CONFIRMED_BLOCKING or NOT_CONFIRMED. evidence_code must be one
|
|
| 5871 |
{block_id: translations[block_id] for block_id in sorted(touched_blocks)},
|
| 5872 |
target_lang, frozen_glossary, section_name=section_name or 'frozen section', max_attempts=2,
|
| 5873 |
)
|
|
|
|
|
|
|
|
|
|
| 5874 |
if quality_issues:
|
| 5875 |
emit_stage('qa')
|
| 5876 |
quality_issues = self._adjudicate_persistent_translation_issues(
|
| 5877 |
source_blocks, translations, quality_issues, target_lang, frozen_glossary,
|
| 5878 |
section_name=section_name or 'frozen section',
|
| 5879 |
)
|
|
|
|
|
|
|
|
|
|
| 5880 |
if quality_issues:
|
| 5881 |
detail = '; '.join(
|
| 5882 |
f'SP-P{block_id}-{claim_id}: {",".join(codes)}'
|
|
@@ -5913,6 +6060,8 @@ Decision must be CONFIRMED_BLOCKING or NOT_CONFIRMED. evidence_code must be one
|
|
| 5913 |
'fact_ledger_hash': self.translation_fact_ledger_hash(value),
|
| 5914 |
'language_adapter': profile['adapter_id'],
|
| 5915 |
'diagnostics': diagnostics[-20:],
|
|
|
|
|
|
|
| 5916 |
}
|
| 5917 |
except PipelineCancelledError:
|
| 5918 |
raise
|
|
|
|
| 4767 |
mapping = dict(existing or {})
|
| 4768 |
rows = (dict(concept_ledger or {}).get('items') or []) if isinstance(concept_ledger, dict) else []
|
| 4769 |
sources = []
|
| 4770 |
+
strict_sources = []
|
| 4771 |
+
flexible_sources = []
|
| 4772 |
for row in rows:
|
| 4773 |
if not isinstance(row, dict):
|
| 4774 |
continue
|
| 4775 |
term = re.sub(r'\s+', ' ', str(row.get('source') or '')).strip()
|
| 4776 |
+
if not term or term in sources:
|
| 4777 |
+
continue
|
| 4778 |
+
sources.append(term)
|
| 4779 |
+
policy = str(row.get('policy') or 'STRICT_CONSTRUCT').strip().upper()
|
| 4780 |
+
if policy == 'FLEXIBLE_TERM':
|
| 4781 |
+
flexible_sources.append(term)
|
| 4782 |
+
else:
|
| 4783 |
+
strict_sources.append(term)
|
| 4784 |
+
# Only true research constructs receive deterministic canonical-identity
|
| 4785 |
+
# enforcement. Descriptive academic terms still receive a preferred
|
| 4786 |
+
# glossary translation but may vary naturally with target-language
|
| 4787 |
+
# grammar or equivalent wording; semantic QA remains authoritative.
|
| 4788 |
+
self._canonical_concept_sources = set(strict_sources)
|
| 4789 |
+
self._canonical_flexible_sources = set(flexible_sources)
|
| 4790 |
if not sources:
|
| 4791 |
return mapping
|
| 4792 |
|
|
|
|
| 5015 |
'UNCERTAINTY_DRIFT','TERMINOLOGY','GRAMMAR','LANGUAGE_CONTAMINATION',
|
| 5016 |
'CITATION_ATTACHMENT','ACRONYM_ATTACHMENT','SEMANTIC_DRIFT',
|
| 5017 |
}
|
| 5018 |
+
allowed_semantic_evidence = {
|
| 5019 |
+
'ACTOR_CHANGED','RELATION_CHANGED','OBJECT_CHANGED','POLARITY_CHANGED',
|
| 5020 |
+
'CAUSALITY_STRENGTH_CHANGED','MODALITY_UNCERTAINTY_CHANGED',
|
| 5021 |
+
'CONDITION_SCOPE_CHANGED','COMPARISON_DIRECTION_CHANGED','TEMPORAL_SCOPE_CHANGED',
|
| 5022 |
+
'CLAIM_MISSING','CLAIM_ADDED','MEANING_CHANGED',
|
| 5023 |
+
}
|
| 5024 |
+
if not isinstance(getattr(self, '_translation_claim_qa_evidence', None), dict):
|
| 5025 |
+
self._translation_claim_qa_evidence = {}
|
| 5026 |
claim_rows = []
|
| 5027 |
glossary_value = dict(glossary or {})
|
| 5028 |
for block_id in expected_ids:
|
|
|
|
| 5061 |
Review EACH claim independently. Do not rewrite and do not provide free-form explanations. Check semantic equivalence and language quality: omissions/additions, negation, uncertainty, association-versus-causation strength, terminology, grammar, language contamination, citation attachment, and acronym attachment.
|
| 5062 |
LANGUAGE ADAPTER: {profile['adapter_id']}; morphology={profile['morphology']}. {profile['term_policy']} {profile['contamination_policy']}
|
| 5063 |
For Russian, do not flag a terminology error merely because a translated term is inflected for grammatical case/number/gender. Russian LANGUAGE_CONTAMINATION includes sustained unprotected English prose and ordinary untranslated English lexemes; never use it for protected acronyms, AI/LLM/SEM, model or tool names, variable names, citations, canonical glossary terms, or source-matched proper names/titles. For Chinese, ordinary untranslated English lexical words are contamination while protected technical objects remain legal. For Kazakh, do not flag a terminology error merely because productive suffixes attach to the correct term stem. Kazakh LANGUAGE_CONTAMINATION includes ordinary untranslated English lexemes and clearly Russian lexical/syntactic intrusions not justified by shared international terminology; Cyrillic script or absence of Kazakh-specific letters alone is never sufficient evidence.
|
| 5064 |
+
Return valid JSON only in this exact form: {{"items":[{{"id":"SP-P000001-C000001","ok":true,"codes":[],"evidence_codes":[]}}]}}.
|
| 5065 |
+
If ok=false, codes MUST contain only values from allowed_issue_codes. For SEMANTIC_DRIFT, NEGATION_DRIFT, CAUSALITY_STRENGTH_DRIFT, UNCERTAINTY_DRIFT, OMITTED_CLAIM, or ADDED_CLAIM, evidence_codes should identify the affected semantic dimension using only: {sorted(allowed_semantic_evidence)}. Use the narrowest supported evidence; do not invent an error dimension. Never return numbered prose, examples, quotes, or corrective wording. Never return replacement text. Return every supplied id exactly once."""
|
| 5066 |
response = self.call_deepseek(
|
| 5067 |
system_prompt, json.dumps(payload, ensure_ascii=False),
|
| 5068 |
max_tokens=max(1200, min(3600, 650 + 260 * len(rows))),
|
|
|
|
| 5083 |
meta = row_lookup[row_id]
|
| 5084 |
raw_codes = [str(code or '').strip().upper() for code in (returned_row.get('codes') or [])]
|
| 5085 |
codes = [code for code in raw_codes if code in allowed_codes]
|
| 5086 |
+
evidence_codes = [
|
| 5087 |
+
str(code or '').strip().upper() for code in (returned_row.get('evidence_codes') or [])
|
| 5088 |
+
if str(code or '').strip().upper() in allowed_semantic_evidence
|
| 5089 |
+
]
|
| 5090 |
+
semantic_code_set = {
|
| 5091 |
+
'SEMANTIC_DRIFT','NEGATION_DRIFT','CAUSALITY_STRENGTH_DRIFT',
|
| 5092 |
+
'UNCERTAINTY_DRIFT','OMITTED_CLAIM','ADDED_CLAIM',
|
| 5093 |
+
}
|
| 5094 |
+
if any(code in semantic_code_set for code in codes):
|
| 5095 |
+
derived = {
|
| 5096 |
+
'NEGATION_DRIFT': 'POLARITY_CHANGED',
|
| 5097 |
+
'CAUSALITY_STRENGTH_DRIFT': 'CAUSALITY_STRENGTH_CHANGED',
|
| 5098 |
+
'UNCERTAINTY_DRIFT': 'MODALITY_UNCERTAINTY_CHANGED',
|
| 5099 |
+
'OMITTED_CLAIM': 'CLAIM_MISSING',
|
| 5100 |
+
'ADDED_CLAIM': 'CLAIM_ADDED',
|
| 5101 |
+
}
|
| 5102 |
+
for issue_code in codes:
|
| 5103 |
+
fallback = derived.get(issue_code)
|
| 5104 |
+
if fallback and fallback not in evidence_codes:
|
| 5105 |
+
evidence_codes.append(fallback)
|
| 5106 |
+
self._translation_claim_qa_evidence[(meta['block_id'], meta['claim_id'])] = {
|
| 5107 |
+
'semantic_delta_codes': evidence_codes[:6],
|
| 5108 |
+
}
|
| 5109 |
model_contamination = 'LANGUAGE_CONTAMINATION' in codes
|
| 5110 |
lexical = self._translation_lexical_purity_evidence(
|
| 5111 |
meta['english'], meta['target'], target_lang, relevant_glossary
|
|
|
|
| 5517 |
]
|
| 5518 |
if lexical_tokens:
|
| 5519 |
evidence_payload['detected_unprotected_english_tokens'] = lexical_tokens[:8]
|
| 5520 |
+
semantic_delta_codes = [
|
| 5521 |
+
str(code or '').strip().upper() for code in (supplied_evidence.get('semantic_delta_codes') or [])
|
| 5522 |
+
if str(code or '').strip()
|
| 5523 |
+
]
|
| 5524 |
+
if semantic_delta_codes:
|
| 5525 |
+
evidence_payload['semantic_delta_codes'] = semantic_delta_codes[:6]
|
| 5526 |
effective_prompt = system_prompt + (
|
| 5527 |
'\nCLAIM QUALITY RETRY. Repair only this one claim. The only permitted repair signals are the following bounded issue codes: '
|
| 5528 |
+ json.dumps(codes[:5], ensure_ascii=False)
|
| 5529 |
+ '. Use the bounded detector evidence below only to repair the supplied claim; do not invent additional defects. '
|
| 5530 |
'The current target claim is context, not authority: the frozen English source remains authoritative. '
|
| 5531 |
'If detected_unprotected_english_tokens is non-empty, every listed token is deterministic lexical-purity evidence and MUST be translated or removed from ordinary target-language prose; do not preserve it unless it is represented by a protected SP-T anchor. '
|
| 5532 |
+
'If semantic_delta_codes is non-empty, repair ONLY those evidenced semantic dimensions against the frozen English source and preserve all unaffected actor, relation, object, polarity, modality, condition, comparison, temporal scope, and evidentiary strength. Do not freely paraphrase a claim that only needs a bounded semantic correction. '
|
| 5533 |
'Preserve every SP-P, SP-C, and SP-T marker byte-for-byte. Do not add claims or factual content. Return only the marked translated claim.'
|
| 5534 |
+ '\n<CLAIM_REPAIR_EVIDENCE>\n'
|
| 5535 |
+ json.dumps(evidence_payload, ensure_ascii=False)
|
|
|
|
| 5604 |
_order, segments = self._parse_translation_claim_segments(restored, [claim_id])
|
| 5605 |
return segments[claim_id]
|
| 5606 |
|
| 5607 |
+
@classmethod
|
| 5608 |
+
def _snapshot_translation_claim_states(cls, source_blocks, translations, pending_issues=None, repair_evidence=None):
|
| 5609 |
+
"""Freeze claim-level target text so a deferred section never needs wholesale retranslation."""
|
| 5610 |
+
pending = dict(pending_issues or {})
|
| 5611 |
+
evidence_map = dict(repair_evidence or {})
|
| 5612 |
+
states = {}
|
| 5613 |
+
for block_id, source_block in dict(source_blocks or {}).items():
|
| 5614 |
+
if block_id not in dict(translations or {}):
|
| 5615 |
+
continue
|
| 5616 |
+
source_claims = cls._translation_claim_units(source_block)
|
| 5617 |
+
claim_order = [item['claim_id'] for item in source_claims]
|
| 5618 |
+
try:
|
| 5619 |
+
_order, target_claims = cls._parse_translation_claim_segments(translations[block_id], claim_order)
|
| 5620 |
+
except Exception:
|
| 5621 |
+
continue
|
| 5622 |
+
for item in source_claims:
|
| 5623 |
+
claim_id = item['claim_id']
|
| 5624 |
+
target_text = str(target_claims.get(claim_id) or '').strip()
|
| 5625 |
+
if not target_text:
|
| 5626 |
+
continue
|
| 5627 |
+
codes = list((pending.get(block_id) or {}).get(claim_id) or [])
|
| 5628 |
+
states[f'SP-P{block_id}-{claim_id}'] = {
|
| 5629 |
+
'block_id': str(block_id),
|
| 5630 |
+
'claim_id': str(claim_id),
|
| 5631 |
+
'source_hash': hashlib.sha256(str(item['text']).encode('utf-8')).hexdigest(),
|
| 5632 |
+
'target_text': target_text,
|
| 5633 |
+
'qa_status': 'repair_pending' if codes else 'passed',
|
| 5634 |
+
'issue_codes': [str(code or '').strip().upper() for code in codes if str(code or '').strip()][:5],
|
| 5635 |
+
'repair_evidence': dict(evidence_map.get((block_id, claim_id)) or {}),
|
| 5636 |
+
}
|
| 5637 |
+
return states
|
| 5638 |
+
|
| 5639 |
+
@classmethod
|
| 5640 |
+
def _restore_translation_claim_states(cls, source_blocks, claim_states):
|
| 5641 |
+
"""Reassemble marked blocks only when every frozen source claim has a hash-matched durable target."""
|
| 5642 |
+
states = dict(claim_states or {})
|
| 5643 |
+
if not states:
|
| 5644 |
+
return {}, {}, False
|
| 5645 |
+
translations = {}
|
| 5646 |
+
pending = {}
|
| 5647 |
+
for block_id, source_block in dict(source_blocks or {}).items():
|
| 5648 |
+
source_claims = cls._translation_claim_units(source_block)
|
| 5649 |
+
order = [item['claim_id'] for item in source_claims]
|
| 5650 |
+
segments = {}
|
| 5651 |
+
for item in source_claims:
|
| 5652 |
+
claim_id = item['claim_id']
|
| 5653 |
+
key = f'SP-P{block_id}-{claim_id}'
|
| 5654 |
+
row = states.get(key)
|
| 5655 |
+
expected_hash = hashlib.sha256(str(item['text']).encode('utf-8')).hexdigest()
|
| 5656 |
+
if not isinstance(row, dict) or row.get('source_hash') != expected_hash or not str(row.get('target_text') or '').strip():
|
| 5657 |
+
return {}, {}, False
|
| 5658 |
+
segments[claim_id] = str(row.get('target_text') or '').strip()
|
| 5659 |
+
if str(row.get('qa_status') or '') == 'repair_pending':
|
| 5660 |
+
codes = [str(code or '').strip().upper() for code in (row.get('issue_codes') or []) if str(code or '').strip()]
|
| 5661 |
+
if codes:
|
| 5662 |
+
pending.setdefault(str(block_id), {})[str(claim_id)] = codes[:5]
|
| 5663 |
+
translations[str(block_id)] = cls._compose_translation_claim_segments(order, segments)
|
| 5664 |
+
return translations, pending, True
|
| 5665 |
+
|
| 5666 |
def _adjudicate_persistent_translation_issues(
|
| 5667 |
self, sources, translations, issues, target_lang, glossary=None, section_name=''
|
| 5668 |
):
|
|
|
|
| 5850 |
)
|
| 5851 |
return {}
|
| 5852 |
|
| 5853 |
+
def translate_frozen_section(self, section_text, target_lang, glossary=None, section_name='', max_attempts=2, stage_callback=None, claim_states=None):
|
| 5854 |
"""Translate one frozen English unit and publish only after all QA passes."""
|
| 5855 |
value = str(section_text or '').strip()
|
| 5856 |
if not value:
|
|
|
|
| 5879 |
+ '\nUse this frozen terminology glossary consistently. Never remap an existing term:\n'
|
| 5880 |
+ json.dumps(frozen_glossary, ensure_ascii=False)
|
| 5881 |
)
|
| 5882 |
+
source_blocks = {
|
| 5883 |
+
block_id: source
|
| 5884 |
+
for batch in batches
|
| 5885 |
+
for block_id, source in batch
|
| 5886 |
+
}
|
| 5887 |
attempts = max(1, int(max_attempts or 1))
|
| 5888 |
last_error = None
|
| 5889 |
+
self._last_translation_claim_states = {}
|
| 5890 |
+
self._translation_claim_qa_evidence = {}
|
| 5891 |
|
| 5892 |
def emit_stage(stage):
|
| 5893 |
if not callable(stage_callback):
|
|
|
|
| 5905 |
emit_stage('translating')
|
| 5906 |
translations = {}
|
| 5907 |
try:
|
| 5908 |
+
translations, quality_issues, resumed_claims = self._restore_translation_claim_states(
|
| 5909 |
+
source_blocks, claim_states
|
| 5910 |
+
)
|
| 5911 |
+
if resumed_claims:
|
| 5912 |
+
for row in dict(claim_states or {}).values():
|
| 5913 |
+
if not isinstance(row, dict):
|
| 5914 |
+
continue
|
| 5915 |
+
block_key = str(row.get('block_id') or '')
|
| 5916 |
+
claim_key = str(row.get('claim_id') or '')
|
| 5917 |
+
evidence = dict(row.get('repair_evidence') or {})
|
| 5918 |
+
if block_key and claim_key and evidence:
|
| 5919 |
+
self._translation_claim_qa_evidence[(block_key, claim_key)] = evidence
|
| 5920 |
+
logging.info(
|
| 5921 |
+
'Translation claim-level durable resume | section=%s claims=%s pending=%s',
|
| 5922 |
+
section_name or 'section', len(dict(claim_states or {})),
|
| 5923 |
+
sum(len(v or {}) for v in quality_issues.values()),
|
| 5924 |
+
)
|
| 5925 |
+
else:
|
| 5926 |
+
for batch in batches:
|
| 5927 |
+
translations.update(self._translate_marked_batch_with_recovery(batch, system_prompt, target_lang))
|
| 5928 |
+
emit_stage('qa')
|
| 5929 |
+
quality_issues = self._audit_translation_blocks_with_retry(
|
| 5930 |
+
source_blocks, translations, target_lang, frozen_glossary,
|
| 5931 |
+
section_name=section_name or 'frozen section', max_attempts=2,
|
| 5932 |
+
)
|
| 5933 |
+
self._last_translation_claim_states = self._snapshot_translation_claim_states(
|
| 5934 |
+
source_blocks, translations, quality_issues, self._translation_claim_qa_evidence
|
| 5935 |
)
|
| 5936 |
local_round = 0
|
| 5937 |
while quality_issues and local_round < 2:
|
|
|
|
| 5962 |
claim_order = list(source_claims)
|
| 5963 |
_order, segments = self._parse_translation_claim_segments(translations[block_id], claim_order)
|
| 5964 |
current_target_claim = segments[claim_id]
|
| 5965 |
+
repair_evidence = dict(
|
| 5966 |
+
self._translation_claim_qa_evidence.get((block_id, claim_id)) or {}
|
| 5967 |
+
)
|
| 5968 |
+
if repair_evidence.get('semantic_delta_codes'):
|
| 5969 |
+
self._record_translation_assurance(
|
| 5970 |
+
'SEMANTIC_REPAIR_EVIDENCE', target_lang=target_lang,
|
| 5971 |
+
block_id=f'SP-P{block_id}-{claim_id}', severity='repair', decision='REPAIR_LOCAL',
|
| 5972 |
+
detail={'deltas': '|'.join(repair_evidence.get('semantic_delta_codes') or [])[:180]},
|
| 5973 |
+
)
|
| 5974 |
if 'LANGUAGE_CONTAMINATION' in {str(code or '').strip().upper() for code in (codes or [])}:
|
| 5975 |
lexical = self._translation_lexical_purity_evidence(
|
| 5976 |
source_claims[claim_id], current_target_claim, target_lang, frozen_glossary
|
|
|
|
| 6012 |
{block_id: translations[block_id] for block_id in sorted(touched_blocks)},
|
| 6013 |
target_lang, frozen_glossary, section_name=section_name or 'frozen section', max_attempts=2,
|
| 6014 |
)
|
| 6015 |
+
self._last_translation_claim_states = self._snapshot_translation_claim_states(
|
| 6016 |
+
source_blocks, translations, quality_issues, self._translation_claim_qa_evidence
|
| 6017 |
+
)
|
| 6018 |
if quality_issues:
|
| 6019 |
emit_stage('qa')
|
| 6020 |
quality_issues = self._adjudicate_persistent_translation_issues(
|
| 6021 |
source_blocks, translations, quality_issues, target_lang, frozen_glossary,
|
| 6022 |
section_name=section_name or 'frozen section',
|
| 6023 |
)
|
| 6024 |
+
self._last_translation_claim_states = self._snapshot_translation_claim_states(
|
| 6025 |
+
source_blocks, translations, quality_issues, self._translation_claim_qa_evidence
|
| 6026 |
+
)
|
| 6027 |
if quality_issues:
|
| 6028 |
detail = '; '.join(
|
| 6029 |
f'SP-P{block_id}-{claim_id}: {",".join(codes)}'
|
|
|
|
| 6060 |
'fact_ledger_hash': self.translation_fact_ledger_hash(value),
|
| 6061 |
'language_adapter': profile['adapter_id'],
|
| 6062 |
'diagnostics': diagnostics[-20:],
|
| 6063 |
+
'claim_states': dict(self._last_translation_claim_states),
|
| 6064 |
+
'claim_state_version': 'translation-claim-state-v1',
|
| 6065 |
}
|
| 6066 |
except PipelineCancelledError:
|
| 6067 |
raise
|