S0-R1-C5.5-R6-R9 Translation Semantic Object Frame

#97
Files changed (2) hide show
  1. core/generation_runner.py +1 -1
  2. core/pipeline_agent.py +228 -23
core/generation_runner.py CHANGED
@@ -45,7 +45,7 @@ CITATION_ROLE_RULES = {
45
  "methods": (2, 280),
46
  "discussion": (2, 220),
47
  }
48
- TRANSLATION_CONTRACT_VERSION = 'hierarchical-multilingual-v8'
49
  FINAL_CITATION_LEDGER_VERSION = 'resolved-final-ledger-v1'
50
  ENGLISH_SECTION_SEAL_VERSION = 'english-section-integrity-seal-v1'
51
 
 
45
  "methods": (2, 280),
46
  "discussion": (2, 220),
47
  }
48
+ TRANSLATION_CONTRACT_VERSION = 'hierarchical-multilingual-v9'
49
  FINAL_CITATION_LEDGER_VERSION = 'resolved-final-ledger-v1'
50
  ENGLISH_SECTION_SEAL_VERSION = 'english-section-integrity-seal-v1'
51
 
core/pipeline_agent.py CHANGED
@@ -70,7 +70,8 @@ class CitationAuditError(DeepSeekRequestError):
70
 
71
 
72
  class SSciPipelineAgent:
73
- TRANSLATION_CONTRACT_VERSION = 'hierarchical-multilingual-v8'
 
74
  MAX_TRANSLATION_CLAIM_REPAIR_ATTEMPTS = 2
75
  MAX_TRANSLATION_CLAIM_TOTAL_REPAIRS = 6
76
  PROTECTED_ENTITY_LEDGER_VERSION = 'protected-entity-ledger-v2'
@@ -6890,6 +6891,116 @@ TEXT:
6890
  frame["frame_hash"] = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
6891
  return frame
6892
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6893
  @classmethod
6894
  def _translation_claim_units(cls, value):
6895
  """Create stable source-side claim IDs without constraining target punctuation."""
@@ -8201,7 +8312,7 @@ SECTION: {section_name}
8201
  'ACTOR_CHANGED','RELATION_CHANGED','OBJECT_CHANGED','POLARITY_CHANGED',
8202
  'CAUSALITY_STRENGTH_CHANGED','MODALITY_UNCERTAINTY_CHANGED',
8203
  'CONDITION_SCOPE_CHANGED','COMPARISON_DIRECTION_CHANGED','TEMPORAL_SCOPE_CHANGED',
8204
- 'CLAIM_MISSING','CLAIM_ADDED','MEANING_CHANGED',
8205
  }
8206
  allowed_grammar_evidence = {
8207
  'CASE_GOVERNMENT','AGREEMENT','MORPHOLOGY','VERB_FORM','WORD_ORDER',
@@ -8228,6 +8339,7 @@ SECTION: {section_name}
8228
  'english': item['text'],
8229
  'target': target_claims[claim_id],
8230
  'source_semantic_frame': self._source_semantic_claim_frame(item['text'], section_name),
 
8231
  })
8232
 
8233
  issues = {}
@@ -8245,16 +8357,18 @@ SECTION: {section_name}
8245
  'target_language': target_lang,
8246
  'glossary': relevant_glossary,
8247
  'allowed_issue_codes': sorted(allowed_codes),
8248
- 'items': [{'id': row['id'], 'english': row['english'], 'target': row['target'], 'source_semantic_frame': row['source_semantic_frame']} for row in rows],
 
8249
  }
8250
  profile = self._translation_language_profile(target_lang)
8251
  lexical_contract = self._translation_lexical_retention_contract(target_lang)
8252
  system_prompt = f"""You are a bilingual academic translation QA classifier for {target_lang}.
8253
- Review EACH claim independently. Do not rewrite and do not provide free-form explanations. The supplied source_semantic_frame is server-frozen source authority: the target must preserve its polarity, causal_strength, modality, claim_provenance, evidence_owner, citation_scope, and citation_count. Check semantic equivalence and language quality: omissions/additions, negation, uncertainty, association-versus-causation strength, terminology, grammar, language contamination, citation attachment, and acronym attachment.
 
8254
  LANGUAGE ADAPTER: {profile['adapter_id']}; morphology={profile['morphology']}. {profile['term_policy']} {lexical_contract} {profile['contamination_policy']}
8255
  For Russian, do not flag a terminology error merely because a translated term is inflected for grammatical case/number/gender; sustained non-Russian prose remains an additional contamination signal. For Kazakh, do not flag a terminology error merely because productive suffixes attach to the correct term stem; clearly Russian lexical/syntactic intrusion remains an additional language-specific signal when the pair supports that conclusion. Cyrillic script or absence of Kazakh-specific letters alone is never sufficient evidence.
8256
- Return valid JSON only in this exact form: {{"items":[{{"id":"SP-P000001-C000001","ok":true,"codes":[],"evidence_codes":[]}}]}}.
8257
- 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)}. For GRAMMAR, evidence_codes should identify the narrow grammatical defect using only: {sorted(allowed_grammar_evidence)}. For LANGUAGE_CONTAMINATION, evidence_codes MUST identify the supported subtype using only: {sorted(allowed_contamination_evidence)}. Use UNTRANSLATED_LEXEME/UNTRANSLATED_PROSE only for Latin/English residue. For Kazakh, use KK_RUSSIAN_LEXICAL_INTRUSION or KK_RUSSIAN_SYNTACTIC_INTRUSION only when the English/target pair itself clearly supports Russian substitution or a Russian syntactic calque; Cyrillic script or absence of Kazakh-specific letters is never sufficient. Use the narrowest supported evidence; use MEANING_CHANGED or GRAMMAR_UNSPECIFIED only when the narrower dimension cannot be identified from the pair. 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."""
8258
  response = self.call_deepseek(
8259
  system_prompt, json.dumps(payload, ensure_ascii=False),
8260
  max_tokens=max(1200, min(3600, 650 + 260 * len(rows))),
@@ -8288,6 +8402,14 @@ If ok=false, codes MUST contain only values from allowed_issue_codes. For SEMANT
8288
  'SEMANTIC_DRIFT','NEGATION_DRIFT','CAUSALITY_STRENGTH_DRIFT',
8289
  'UNCERTAINTY_DRIFT','OMITTED_CLAIM','ADDED_CLAIM',
8290
  }
 
 
 
 
 
 
 
 
8291
  if any(code in semantic_code_set for code in codes):
8292
  derived = {
8293
  'NEGATION_DRIFT': 'POLARITY_CHANGED',
@@ -8300,12 +8422,22 @@ If ok=false, codes MUST contain only values from allowed_issue_codes. For SEMANT
8300
  fallback = derived.get(issue_code)
8301
  if fallback and fallback not in evidence_codes:
8302
  evidence_codes.append(fallback)
8303
- if 'SEMANTIC_DRIFT' in codes and not evidence_codes:
8304
- evidence_codes.append('MEANING_CHANGED')
 
 
 
 
 
 
 
8305
  evidence_row = self._translation_claim_qa_evidence.setdefault(
8306
  (meta['block_id'], meta['claim_id']), {}
8307
  )
8308
  evidence_row['semantic_delta_codes'] = evidence_codes[:6]
 
 
 
8309
  if 'GRAMMAR' in codes:
8310
  if not grammar_evidence_codes:
8311
  grammar_evidence_codes = ['GRAMMAR_UNSPECIFIED']
@@ -8440,7 +8572,15 @@ If ok=false, codes MUST contain only values from allowed_issue_codes. For SEMANT
8440
  )
8441
  if only_suppressed_contamination:
8442
  continue
8443
- codes = ['SEMANTIC_DRIFT']
 
 
 
 
 
 
 
 
8444
  issues.setdefault(meta['block_id'], {})[meta['claim_id']] = list(dict.fromkeys(codes))[:5]
8445
  return issues
8446
 
@@ -8804,6 +8944,12 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
8804
  'grammar_evidence_codes': sorted({str(code or '').strip().upper() for code in (evidence.get('grammar_evidence_codes') or []) if str(code or '').strip()}),
8805
  'contamination_evidence_codes': sorted({str(code or '').strip().upper() for code in (evidence.get('contamination_evidence_codes') or []) if str(code or '').strip()}),
8806
  'detected_unprotected_english_tokens': sorted({str(token or '').strip().casefold() for token in (evidence.get('detected_unprotected_english_tokens') or []) if str(token or '').strip()}),
 
 
 
 
 
 
8807
  }
8808
  payload = json.dumps(bounded, ensure_ascii=False, sort_keys=True, separators=(',', ':'))
8809
  return hashlib.sha256(payload.encode('utf-8')).hexdigest()
@@ -8864,7 +9010,12 @@ Each code must be one of: {sorted(allowed)}. Return one or more specific dimensi
8864
  strategy = str(repair_strategy or 'LOCAL_EDIT').strip().upper()
8865
  if strategy not in {'LOCAL_EDIT', 'SOURCE_REGENERATE'}:
8866
  strategy = 'LOCAL_EDIT'
8867
- evidence_payload = {'issue_codes': codes[:5], 'repair_strategy': strategy}
 
 
 
 
 
8868
  if strategy == 'LOCAL_EDIT':
8869
  evidence_payload['current_target_claim'] = str(current_target_claim or '').strip()
8870
  lexical_tokens = [
@@ -8879,6 +9030,12 @@ Each code must be one of: {sorted(allowed)}. Return one or more specific dimensi
8879
  ]
8880
  if semantic_delta_codes:
8881
  evidence_payload['semantic_delta_codes'] = semantic_delta_codes[:6]
 
 
 
 
 
 
8882
  grammar_evidence_codes = [
8883
  str(code or '').strip().upper() for code in (supplied_evidence.get('grammar_evidence_codes') or [])
8884
  if str(code or '').strip()
@@ -8903,7 +9060,7 @@ Each code must be one of: {sorted(allowed)}. Return one or more specific dimensi
8903
  + '. Use the bounded detector evidence below only to repair the supplied claim; do not invent additional defects. '
8904
  'For LOCAL_EDIT, the current target claim is context, not authority; for SOURCE_REGENERATE it is intentionally absent. The frozen English source is always authoritative. '
8905
  'If detected_unprotected_english_tokens is non-empty, every listed token is deterministic lexical-purity evidence from a prior target and MUST NOT survive as ordinary untranslated target-language prose unless represented by a protected SP-T anchor. '
8906
- 'If semantic_delta_codes is non-empty, enforce 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. UNRESOLVED_SEMANTIC_DELTA is a signal to regenerate faithfully from source, not an instruction to guess a specific correction. '
8907
  'If grammar_evidence_codes is non-empty, repair or avoid only those bounded grammatical defect classes; GRAMMAR_UNSPECIFIED requires a fresh natural target-language rendering rather than blind micro-editing. '
8908
  'If contamination_evidence_codes identifies KK_RUSSIAN_LEXICAL_INTRUSION or KK_RUSSIAN_SYNTACTIC_INTRUSION, remove only the evidenced Russian substitution/calque while preserving the frozen English meaning; do not invent an English lexical defect when deterministic English-token evidence is absent. '
8909
  '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.'
@@ -9014,6 +9171,9 @@ Each code must be one of: {sorted(allowed)}. Return one or more specific dimensi
9014
  'source_hash': hashlib.sha256(str(item['text']).encode('utf-8')).hexdigest(),
9015
  'semantic_frame': cls._source_semantic_claim_frame(item['text'], section_name),
9016
  'semantic_frame_hash': cls._source_semantic_claim_frame(item['text'], section_name).get('frame_hash', ''),
 
 
 
9017
  'target_text': target_text,
9018
  'qa_status': 'repair_pending' if codes else 'passed',
9019
  'issue_codes': [str(code or '').strip().upper() for code in codes if str(code or '').strip()][:5],
@@ -9058,11 +9218,16 @@ Each code must be one of: {sorted(allowed)}. Return one or more specific dimensi
9058
  row = states.get(key)
9059
  expected_hash = hashlib.sha256(str(item['text']).encode('utf-8')).hexdigest()
9060
  expected_semantic_hash = cls._source_semantic_claim_frame(item['text'], section_name).get('frame_hash', '')
 
9061
  if not isinstance(row, dict) or row.get('source_hash') != expected_hash or not str(row.get('target_text') or '').strip():
9062
  return {}, {}, False
9063
- # R6-R1: new states are semantic-frame bound. Pre-R6 states do
9064
- # not carry this optional field and retain legacy fail-closed
9065
- # repair history instead of being silently "revived".
 
 
 
 
9066
  stored_semantic_hash = str(row.get('semantic_frame_hash') or '')
9067
  if stored_semantic_hash and stored_semantic_hash != expected_semantic_hash:
9068
  return {}, {}, False
@@ -9177,11 +9342,25 @@ Each code must be one of: {sorted(allowed)}. Return one or more specific dimensi
9177
  for item in (repair_evidence.get('grammar_evidence_codes') or [])
9178
  if str(item or '').strip()
9179
  ] or ['GRAMMAR_UNSPECIFIED']
 
 
 
 
 
 
 
 
 
9180
  rows.append({
9181
  'id': f'SP-P{block_id}-{claim_id}', 'block_id': block_id,
9182
  'claim_id': claim_id, 'code': code, 'english': source_claims[claim_id],
9183
  'target': target_claims[claim_id], 'deterministic_confirmed': False,
9184
  'grammar_evidence_codes': grammar_evidence_codes[:4],
 
 
 
 
 
9185
  })
9186
 
9187
  confirmed = {}
@@ -9202,8 +9381,10 @@ Each code must be one of: {sorted(allowed)}. Return one or more specific dimensi
9202
  return confirmed
9203
 
9204
  allowed_evidence = {
9205
- 'CLAIM_MISSING','CLAIM_ADDED','POLARITY_CHANGED','CAUSALITY_CHANGED',
9206
- 'UNCERTAINTY_CHANGED','MEANING_CHANGED','CITATION_MOVED','ACRONYM_MISATTACHED',
 
 
9207
  'GRAMMATICAL_VIOLATION','CASE_GOVERNMENT','AGREEMENT','MORPHOLOGY','VERB_FORM',
9208
  'SYNTAX_STRUCTURE','PUNCTUATION','SYNTACTIC_COMPLETENESS','GRAMMAR_UNSPECIFIED',
9209
  'WORD_ORDER_UNGRAMMATICAL','WORD_ORDER_SEMANTIC_SCOPE_CHANGED',
@@ -9228,6 +9409,9 @@ Each code must be one of: {sorted(allowed)}. Return one or more specific dimensi
9228
  },
9229
  'contamination_evidence_codes': list(row.get('contamination_evidence_codes') or [])[:2],
9230
  'grammar_evidence_codes': list(row.get('grammar_evidence_codes') or [])[:4],
 
 
 
9231
  } for row in model_rows],
9232
  }
9233
  lexical_contract = self._translation_lexical_retention_contract(target_lang)
@@ -9236,6 +9420,7 @@ This is verification-only: do not rewrite, suggest wording, or emit free-form ex
9236
  {lexical_contract}
9237
  For Russian, allow required case/number/gender inflection and retain sustained non-Russian prose as an additional contamination signal. For Kazakh, allow productive case, possessive, person/plural and vowel-harmony suffixes on the correct term stem, but treat clearly Russian lexical/syntactic substitution as blocking only when the pair itself supports that conclusion. For Chinese, allow natural reordering and sentence reshaping inside the same claim while preserving meaning.
9238
  For LANGUAGE_CONTAMINATION, deterministic_lexical is authoritative for ordinary English/Latin residue: when confirmed=false and token_count=0, you MUST NOT confirm UNTRANSLATED_LEXEME or UNTRANSLATED_PROSE. For Kazakh, a non-Latin contamination blocker may be confirmed only when contamination_evidence_codes contains KK_RUSSIAN_LEXICAL_INTRUSION or KK_RUSSIAN_SYNTACTIC_INTRUSION, and the evidence_code must match one of those supplied typed codes. The per-item glossary lists the frozen target terms that are legal for that source claim.
 
9239
  For GRAMMAR, grammar_evidence_codes is the typed defect history and must remain continuous into this final decision. Never use generic GRAMMATICAL_VIOLATION as final blocking evidence when a typed grammar history exists. If WORD_ORDER is supplied, non-canonical or stylistically different ordering is NOT blocking. Confirm only when the target itself is ungrammatical because of ordering (WORD_ORDER_UNGRAMMATICAL) or when ordering changes modifier/negation/modality/condition/relation scope relative to the English claim (WORD_ORDER_SEMANTIC_SCOPE_CHANGED). For CASE_GOVERNMENT, AGREEMENT, MORPHOLOGY, VERB_FORM, SYNTAX_STRUCTURE, PUNCTUATION, or SYNTACTIC_COMPLETENESS, a blocking evidence_code must match the supplied typed class. If only GRAMMAR_UNSPECIFIED was supplied, you may narrow it to one of those specific classes or one of the two WORD_ORDER consequence codes; do not confirm a generic grammar label.
9240
  Return valid JSON only: {{"items":[{{"id":"SP-P000001-C000001","issue_code":"GRAMMAR","decision":"CONFIRMED_BLOCKING","evidence_code":"AGREEMENT"}}]}}.
9241
  Decision must be CONFIRMED_BLOCKING or NOT_CONFIRMED. evidence_code must be one of: {sorted(allowed_evidence)}. Return every supplied id+issue_code pair exactly once."""
@@ -9264,6 +9449,25 @@ Decision must be CONFIRMED_BLOCKING or NOT_CONFIRMED. evidence_code must be one
9264
  evidence_code = str(item.get('evidence_code') or '').strip().upper()
9265
  if decision not in {'CONFIRMED_BLOCKING', 'NOT_CONFIRMED'} or evidence_code not in allowed_evidence:
9266
  raise DeepSeekRequestError(f'Translation QA adjudication returned invalid bounded decision for {row["id"]}.')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9267
  if row['code'] == 'LANGUAGE_CONTAMINATION':
9268
  lexical = dict(row.get('lexical_evidence') or {})
9269
  typed = set(row.get('contamination_evidence_codes') or [])
@@ -9643,13 +9847,14 @@ Decision must be CONFIRMED_BLOCKING or NOT_CONFIRMED. evidence_code must be one
9643
  semantic_deltas.append(fallback)
9644
  if semantic_deltas:
9645
  repair_evidence['semantic_delta_codes'] = list(dict.fromkeys(semantic_deltas))[:6]
9646
- specific_semantic = [code for code in semantic_deltas if code != 'MEANING_CHANGED']
9647
- if semantic_issue and (not specific_semantic or 'MEANING_CHANGED' in semantic_deltas):
9648
- specific_semantic = self._decompose_translation_semantic_delta(
9649
- source_claims[claim_id], current_target_claim, target_lang,
9650
- issue_codes=codes, section_name=f'SP-P{block_id}-{claim_id}',
9651
- )
9652
- repair_evidence['semantic_delta_codes'] = list(specific_semantic)[:6]
 
9653
  if repair_evidence.get('semantic_delta_codes'):
9654
  self._record_translation_assurance(
9655
  'SEMANTIC_REPAIR_EVIDENCE', target_lang=target_lang,
 
70
 
71
 
72
  class SSciPipelineAgent:
73
+ TRANSLATION_CONTRACT_VERSION = 'hierarchical-multilingual-v9'
74
+ TRANSLATION_SEMANTIC_OBJECT_VERSION = 'translation-semantic-object-v1'
75
  MAX_TRANSLATION_CLAIM_REPAIR_ATTEMPTS = 2
76
  MAX_TRANSLATION_CLAIM_TOTAL_REPAIRS = 6
77
  PROTECTED_ENTITY_LEDGER_VERSION = 'protected-entity-ledger-v2'
 
6891
  frame["frame_hash"] = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
6892
  return frame
6893
 
6894
+ @classmethod
6895
+ def _translation_semantic_object_frame(cls, source_claim, section_name='', glossary=None):
6896
+ """Compile one frozen English claim into a versioned semantic object.
6897
+
6898
+ R6-R9 moves translation QA away from an untyped whole-sentence verdict.
6899
+ The server owns this object before any target-language judgment: claim
6900
+ identity, evidence/causal/modality contract, protected factual object
6901
+ types, and canonical terminology ownership are frozen once and hashed.
6902
+ The model may judge target realization, but it cannot redefine source
6903
+ semantics or invent a new object schema on retry/resume.
6904
+ """
6905
+ source = re.sub(r"\s+", " ", str(source_claim or '')).strip()
6906
+ semantic_frame = cls._source_semantic_claim_frame(source, section_name)
6907
+ _protected, mapping = cls._protect_translation_tokens(source, claim_id='C000001')
6908
+ type_counts = Counter(str(item.get('type') or 'IMMUTABLE') for item in mapping.values())
6909
+ render_counts = Counter(str(item.get('render_policy') or 'exact') for item in mapping.values())
6910
+ glossary_value = dict(glossary or {})
6911
+ canonical_terms = []
6912
+ folded = source.casefold()
6913
+ for source_term, target_term in sorted(glossary_value.items(), key=lambda item: str(item[0]).casefold()):
6914
+ source_term = re.sub(r"\s+", " ", str(source_term or '')).strip()
6915
+ target_term = re.sub(r"\s+", " ", str(target_term or '')).strip()
6916
+ if source_term and source_term.casefold() in folded:
6917
+ canonical_terms.append({
6918
+ 'source': source_term,
6919
+ 'target': target_term,
6920
+ 'identity': hashlib.sha256(source_term.casefold().encode('utf-8')).hexdigest()[:16],
6921
+ })
6922
+ semantic_contract = {
6923
+ key: semantic_frame.get(key)
6924
+ for key in (
6925
+ 'polarity','causal_strength','modality','claim_provenance','evidence_owner',
6926
+ 'citation_requirement','citation_scope','citation_count'
6927
+ )
6928
+ }
6929
+ obj = {
6930
+ 'version': cls.TRANSLATION_SEMANTIC_OBJECT_VERSION,
6931
+ 'source_hash': hashlib.sha256(source.encode('utf-8')).hexdigest(),
6932
+ 'semantic_contract': semantic_contract,
6933
+ 'protected_object_counts': dict(sorted(type_counts.items())),
6934
+ 'render_policy_counts': dict(sorted(render_counts.items())),
6935
+ }
6936
+ # Identity is source-owned. Target glossary realization is useful QA
6937
+ # context but must not change the semantic-object hash across languages.
6938
+ canonical = json.dumps(obj, ensure_ascii=False, sort_keys=True, separators=(',', ':'))
6939
+ obj['object_hash'] = hashlib.sha256(canonical.encode('utf-8')).hexdigest()
6940
+ obj['canonical_terms'] = canonical_terms[:16]
6941
+ return obj
6942
+
6943
+ @staticmethod
6944
+ def _translation_semantic_delta_objects(source_claim, target_claim, raw_deltas):
6945
+ """Validate model-reported semantic deltas as source/target-bound objects.
6946
+
6947
+ A generic ``SEMANTIC_DRIFT`` label is not evidence. R6-R9 requires a
6948
+ narrow semantic dimension plus spans that actually belong to the frozen
6949
+ source and current target (except claim-added/missing, where one side may
6950
+ be empty). Invalid or over-broad evidence is ignored rather than being
6951
+ promoted to a fatal repair lineage.
6952
+ """
6953
+ allowed = {
6954
+ 'ACTOR_CHANGED','RELATION_CHANGED','OBJECT_CHANGED','CONDITION_SCOPE_CHANGED',
6955
+ 'COMPARISON_DIRECTION_CHANGED','TEMPORAL_SCOPE_CHANGED','CLAIM_MISSING','CLAIM_ADDED',
6956
+ }
6957
+ source = re.sub(r"\s+", " ", str(source_claim or '')).strip()
6958
+ target = re.sub(r"\s+", " ", str(target_claim or '')).strip()
6959
+ source_folded, target_folded = source.casefold(), target.casefold()
6960
+ objects = []
6961
+ for raw in (raw_deltas or []):
6962
+ if not isinstance(raw, dict):
6963
+ continue
6964
+ dimension = str(raw.get('dimension') or '').strip().upper()
6965
+ if dimension not in allowed:
6966
+ continue
6967
+ source_span = re.sub(r"\s+", " ", str(raw.get('source_span') or '')).strip()
6968
+ target_span = re.sub(r"\s+", " ", str(raw.get('target_span') or '')).strip()
6969
+ if dimension == 'CLAIM_MISSING':
6970
+ if not source_span or source_span.casefold() not in source_folded:
6971
+ continue
6972
+ elif dimension == 'CLAIM_ADDED':
6973
+ if not target_span or target_span.casefold() not in target_folded:
6974
+ continue
6975
+ else:
6976
+ if not source_span or not target_span:
6977
+ continue
6978
+ if source_span.casefold() not in source_folded or target_span.casefold() not in target_folded:
6979
+ continue
6980
+ # Whole-sentence restatement is not narrow evidence except for very
6981
+ # short claims, where a smaller span may not exist naturally.
6982
+ if len(source) > 180 and source_span and len(source_span) > max(180, int(len(source) * 0.80)):
6983
+ continue
6984
+ if len(target) > 180 and target_span and len(target_span) > max(180, int(len(target) * 0.80)):
6985
+ continue
6986
+ identity_payload = json.dumps(
6987
+ {'dimension': dimension, 'source_span': source_span.casefold(), 'target_span': target_span.casefold()},
6988
+ ensure_ascii=False, sort_keys=True, separators=(',', ':')
6989
+ )
6990
+ objects.append({
6991
+ 'dimension': dimension, 'source_span': source_span, 'target_span': target_span,
6992
+ 'object_id': hashlib.sha256(identity_payload.encode('utf-8')).hexdigest()[:20],
6993
+ })
6994
+ deduped = []
6995
+ seen = set()
6996
+ for item in objects:
6997
+ key = (item['dimension'], item['object_id'])
6998
+ if key in seen:
6999
+ continue
7000
+ seen.add(key)
7001
+ deduped.append(item)
7002
+ return deduped[:6]
7003
+
7004
  @classmethod
7005
  def _translation_claim_units(cls, value):
7006
  """Create stable source-side claim IDs without constraining target punctuation."""
 
8312
  'ACTOR_CHANGED','RELATION_CHANGED','OBJECT_CHANGED','POLARITY_CHANGED',
8313
  'CAUSALITY_STRENGTH_CHANGED','MODALITY_UNCERTAINTY_CHANGED',
8314
  'CONDITION_SCOPE_CHANGED','COMPARISON_DIRECTION_CHANGED','TEMPORAL_SCOPE_CHANGED',
8315
+ 'CLAIM_MISSING','CLAIM_ADDED',
8316
  }
8317
  allowed_grammar_evidence = {
8318
  'CASE_GOVERNMENT','AGREEMENT','MORPHOLOGY','VERB_FORM','WORD_ORDER',
 
8339
  'english': item['text'],
8340
  'target': target_claims[claim_id],
8341
  'source_semantic_frame': self._source_semantic_claim_frame(item['text'], section_name),
8342
+ 'semantic_object': self._translation_semantic_object_frame(item['text'], section_name, glossary_value),
8343
  })
8344
 
8345
  issues = {}
 
8357
  'target_language': target_lang,
8358
  'glossary': relevant_glossary,
8359
  'allowed_issue_codes': sorted(allowed_codes),
8360
+ 'semantic_object_version': self.TRANSLATION_SEMANTIC_OBJECT_VERSION,
8361
+ 'items': [{'id': row['id'], 'english': row['english'], 'target': row['target'], 'source_semantic_frame': row['source_semantic_frame'], 'semantic_object': row['semantic_object']} for row in rows],
8362
  }
8363
  profile = self._translation_language_profile(target_lang)
8364
  lexical_contract = self._translation_lexical_retention_contract(target_lang)
8365
  system_prompt = f"""You are a bilingual academic translation QA classifier for {target_lang}.
8366
+ Review EACH claim independently. Do not rewrite and do not provide free-form explanations. The supplied semantic_object is the server-frozen source authority. Preserve its semantic_contract, protected-object ownership, and canonical terminology identities. Check semantic equivalence and language quality: omissions/additions, negation, uncertainty, association-versus-causation strength, terminology, grammar, language contamination, citation attachment, and acronym attachment.
8367
+ For a generic SEMANTIC_DRIFT finding, a label alone is NOT evidence. You MUST also return one or more narrow semantic_deltas. Each delta must use dimension ACTOR_CHANGED, RELATION_CHANGED, OBJECT_CHANGED, CONDITION_SCOPE_CHANGED, COMPARISON_DIRECTION_CHANGED, TEMPORAL_SCOPE_CHANGED, CLAIM_MISSING, or CLAIM_ADDED and quote short exact source_span/target_span evidence from the supplied pair. For CLAIM_MISSING target_span may be empty; for CLAIM_ADDED source_span may be empty. Do not return the whole sentence as a pseudo-span when a narrower phrase identifies the changed object. If you cannot identify a source/target-bound semantic object, do NOT emit SEMANTIC_DRIFT.
8368
  LANGUAGE ADAPTER: {profile['adapter_id']}; morphology={profile['morphology']}. {profile['term_policy']} {lexical_contract} {profile['contamination_policy']}
8369
  For Russian, do not flag a terminology error merely because a translated term is inflected for grammatical case/number/gender; sustained non-Russian prose remains an additional contamination signal. For Kazakh, do not flag a terminology error merely because productive suffixes attach to the correct term stem; clearly Russian lexical/syntactic intrusion remains an additional language-specific signal when the pair supports that conclusion. Cyrillic script or absence of Kazakh-specific letters alone is never sufficient evidence.
8370
+ Return valid JSON only in this exact form: {{"items":[{{"id":"SP-P000001-C000001","ok":true,"codes":[],"evidence_codes":[],"semantic_deltas":[]}}]}}.
8371
+ If ok=false, codes MUST contain only values from allowed_issue_codes. For 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)}. SEMANTIC_DRIFT additionally requires validated semantic_deltas as described above; never use a generic MEANING_CHANGED fallback. For GRAMMAR, evidence_codes should identify the narrow grammatical defect using only: {sorted(allowed_grammar_evidence)}. For LANGUAGE_CONTAMINATION, evidence_codes MUST identify the supported subtype using only: {sorted(allowed_contamination_evidence)}. Use UNTRANSLATED_LEXEME/UNTRANSLATED_PROSE only for Latin/English residue. For Kazakh, use KK_RUSSIAN_LEXICAL_INTRUSION or KK_RUSSIAN_SYNTACTIC_INTRUSION only when the English/target pair itself clearly supports Russian substitution or a Russian syntactic calque; Cyrillic script or absence of Kazakh-specific letters is never sufficient. Use the narrowest supported evidence; use GRAMMAR_UNSPECIFIED only when a grammatical defect is clear but cannot be narrowed. 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."""
8372
  response = self.call_deepseek(
8373
  system_prompt, json.dumps(payload, ensure_ascii=False),
8374
  max_tokens=max(1200, min(3600, 650 + 260 * len(rows))),
 
8402
  'SEMANTIC_DRIFT','NEGATION_DRIFT','CAUSALITY_STRENGTH_DRIFT',
8403
  'UNCERTAINTY_DRIFT','OMITTED_CLAIM','ADDED_CLAIM',
8404
  }
8405
+ semantic_objects = self._translation_semantic_delta_objects(
8406
+ meta['english'], meta['target'], returned_row.get('semantic_deltas') or []
8407
+ )
8408
+ if semantic_objects:
8409
+ object_dimensions = [item['dimension'] for item in semantic_objects]
8410
+ for dimension in object_dimensions:
8411
+ if dimension not in evidence_codes:
8412
+ evidence_codes.append(dimension)
8413
  if any(code in semantic_code_set for code in codes):
8414
  derived = {
8415
  'NEGATION_DRIFT': 'POLARITY_CHANGED',
 
8422
  fallback = derived.get(issue_code)
8423
  if fallback and fallback not in evidence_codes:
8424
  evidence_codes.append(fallback)
8425
+ # R6-R9: a generic semantic verdict cannot create a repair
8426
+ # lineage without a source/target-bound semantic object.
8427
+ if 'SEMANTIC_DRIFT' in codes and not semantic_objects:
8428
+ codes = [code for code in codes if code != 'SEMANTIC_DRIFT']
8429
+ self._record_translation_assurance(
8430
+ 'SEMANTIC_OBJECT_UNTYPED_QA_UNCONFIRMED', target_lang=target_lang, block_id=row_id,
8431
+ severity='diagnostic', decision='PASS_WITH_WARNING',
8432
+ detail={'semantic_object_version': self.TRANSLATION_SEMANTIC_OBJECT_VERSION},
8433
+ )
8434
  evidence_row = self._translation_claim_qa_evidence.setdefault(
8435
  (meta['block_id'], meta['claim_id']), {}
8436
  )
8437
  evidence_row['semantic_delta_codes'] = evidence_codes[:6]
8438
+ evidence_row['semantic_object_deltas'] = semantic_objects[:6]
8439
+ evidence_row['semantic_object_hash'] = str(meta['semantic_object'].get('object_hash') or '')
8440
+ evidence_row['semantic_object_version'] = self.TRANSLATION_SEMANTIC_OBJECT_VERSION
8441
  if 'GRAMMAR' in codes:
8442
  if not grammar_evidence_codes:
8443
  grammar_evidence_codes = ['GRAMMAR_UNSPECIFIED']
 
8572
  )
8573
  if only_suppressed_contamination:
8574
  continue
8575
+ # R6-R9: model rejection without a typed actionable defect is
8576
+ # uncertainty, not evidence. Deterministic skeleton/fact/entity
8577
+ # validators remain authoritative and already ran.
8578
+ self._record_translation_assurance(
8579
+ 'TRANSLATION_QA_NO_TYPED_BLOCKER', target_lang=target_lang, block_id=row_id,
8580
+ severity='diagnostic', decision='PASS_WITH_WARNING',
8581
+ detail={'semantic_object_version': self.TRANSLATION_SEMANTIC_OBJECT_VERSION},
8582
+ )
8583
+ continue
8584
  issues.setdefault(meta['block_id'], {})[meta['claim_id']] = list(dict.fromkeys(codes))[:5]
8585
  return issues
8586
 
 
8944
  'grammar_evidence_codes': sorted({str(code or '').strip().upper() for code in (evidence.get('grammar_evidence_codes') or []) if str(code or '').strip()}),
8945
  'contamination_evidence_codes': sorted({str(code or '').strip().upper() for code in (evidence.get('contamination_evidence_codes') or []) if str(code or '').strip()}),
8946
  'detected_unprotected_english_tokens': sorted({str(token or '').strip().casefold() for token in (evidence.get('detected_unprotected_english_tokens') or []) if str(token or '').strip()}),
8947
+ 'semantic_object_version': str(evidence.get('semantic_object_version') or ''),
8948
+ 'semantic_object_delta_ids': sorted({
8949
+ str(item.get('object_id') or item.get('dimension') or '').strip()
8950
+ for item in (evidence.get('semantic_object_deltas') or []) if isinstance(item, dict)
8951
+ and str(item.get('object_id') or item.get('dimension') or '').strip()
8952
+ }),
8953
  }
8954
  payload = json.dumps(bounded, ensure_ascii=False, sort_keys=True, separators=(',', ':'))
8955
  return hashlib.sha256(payload.encode('utf-8')).hexdigest()
 
9010
  strategy = str(repair_strategy or 'LOCAL_EDIT').strip().upper()
9011
  if strategy not in {'LOCAL_EDIT', 'SOURCE_REGENERATE'}:
9012
  strategy = 'LOCAL_EDIT'
9013
+ evidence_payload = {
9014
+ 'issue_codes': codes[:5], 'repair_strategy': strategy,
9015
+ 'semantic_object': self._translation_semantic_object_frame(
9016
+ source_claim, getattr(self, '_translation_section_name', '')
9017
+ ),
9018
+ }
9019
  if strategy == 'LOCAL_EDIT':
9020
  evidence_payload['current_target_claim'] = str(current_target_claim or '').strip()
9021
  lexical_tokens = [
 
9030
  ]
9031
  if semantic_delta_codes:
9032
  evidence_payload['semantic_delta_codes'] = semantic_delta_codes[:6]
9033
+ semantic_object_deltas = [
9034
+ dict(item) for item in (supplied_evidence.get('semantic_object_deltas') or [])
9035
+ if isinstance(item, dict) and str(item.get('dimension') or '').strip()
9036
+ ]
9037
+ if semantic_object_deltas:
9038
+ evidence_payload['semantic_object_deltas'] = semantic_object_deltas[:6]
9039
  grammar_evidence_codes = [
9040
  str(code or '').strip().upper() for code in (supplied_evidence.get('grammar_evidence_codes') or [])
9041
  if str(code or '').strip()
 
9060
  + '. Use the bounded detector evidence below only to repair the supplied claim; do not invent additional defects. '
9061
  'For LOCAL_EDIT, the current target claim is context, not authority; for SOURCE_REGENERATE it is intentionally absent. The frozen English source is always authoritative. '
9062
  'If detected_unprotected_english_tokens is non-empty, every listed token is deterministic lexical-purity evidence from a prior target and MUST NOT survive as ordinary untranslated target-language prose unless represented by a protected SP-T anchor. '
9063
+ 'If semantic_delta_codes is non-empty, enforce 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. When semantic_object_deltas is present, its source_span/target_span pairs are the narrow object-bound evidence; repair that object relation only and do not reinterpret the rest of the claim. UNRESOLVED_SEMANTIC_DELTA is a signal to regenerate faithfully from source, not an instruction to guess a specific correction. '
9064
  'If grammar_evidence_codes is non-empty, repair or avoid only those bounded grammatical defect classes; GRAMMAR_UNSPECIFIED requires a fresh natural target-language rendering rather than blind micro-editing. '
9065
  'If contamination_evidence_codes identifies KK_RUSSIAN_LEXICAL_INTRUSION or KK_RUSSIAN_SYNTACTIC_INTRUSION, remove only the evidenced Russian substitution/calque while preserving the frozen English meaning; do not invent an English lexical defect when deterministic English-token evidence is absent. '
9066
  '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.'
 
9171
  'source_hash': hashlib.sha256(str(item['text']).encode('utf-8')).hexdigest(),
9172
  'semantic_frame': cls._source_semantic_claim_frame(item['text'], section_name),
9173
  'semantic_frame_hash': cls._source_semantic_claim_frame(item['text'], section_name).get('frame_hash', ''),
9174
+ 'semantic_object': cls._translation_semantic_object_frame(item['text'], section_name),
9175
+ 'semantic_object_hash': cls._translation_semantic_object_frame(item['text'], section_name).get('object_hash', ''),
9176
+ 'semantic_object_version': cls.TRANSLATION_SEMANTIC_OBJECT_VERSION,
9177
  'target_text': target_text,
9178
  'qa_status': 'repair_pending' if codes else 'passed',
9179
  'issue_codes': [str(code or '').strip().upper() for code in codes if str(code or '').strip()][:5],
 
9218
  row = states.get(key)
9219
  expected_hash = hashlib.sha256(str(item['text']).encode('utf-8')).hexdigest()
9220
  expected_semantic_hash = cls._source_semantic_claim_frame(item['text'], section_name).get('frame_hash', '')
9221
+ expected_object_hash = cls._translation_semantic_object_frame(item['text'], section_name).get('object_hash', '')
9222
  if not isinstance(row, dict) or row.get('source_hash') != expected_hash or not str(row.get('target_text') or '').strip():
9223
  return {}, {}, False
9224
+ # R6-R9: durable target claims are semantic-object bound. Any
9225
+ # pre-v1 object state is target-only stale and must be rebuilt;
9226
+ # sealed English remains untouched.
9227
+ if str(row.get('semantic_object_version') or '') != cls.TRANSLATION_SEMANTIC_OBJECT_VERSION:
9228
+ return {}, {}, False
9229
+ if str(row.get('semantic_object_hash') or '') != expected_object_hash:
9230
+ return {}, {}, False
9231
  stored_semantic_hash = str(row.get('semantic_frame_hash') or '')
9232
  if stored_semantic_hash and stored_semantic_hash != expected_semantic_hash:
9233
  return {}, {}, False
 
9342
  for item in (repair_evidence.get('grammar_evidence_codes') or [])
9343
  if str(item or '').strip()
9344
  ] or ['GRAMMAR_UNSPECIFIED']
9345
+ semantic_evidence_codes = [
9346
+ str(item or '').strip().upper()
9347
+ for item in (repair_evidence.get('semantic_delta_codes') or [])
9348
+ if str(item or '').strip()
9349
+ ]
9350
+ semantic_object_deltas = [
9351
+ dict(item) for item in (repair_evidence.get('semantic_object_deltas') or [])
9352
+ if isinstance(item, dict)
9353
+ ]
9354
  rows.append({
9355
  'id': f'SP-P{block_id}-{claim_id}', 'block_id': block_id,
9356
  'claim_id': claim_id, 'code': code, 'english': source_claims[claim_id],
9357
  'target': target_claims[claim_id], 'deterministic_confirmed': False,
9358
  'grammar_evidence_codes': grammar_evidence_codes[:4],
9359
+ 'semantic_evidence_codes': semantic_evidence_codes[:6],
9360
+ 'semantic_object_deltas': semantic_object_deltas[:6],
9361
+ 'semantic_object': self._translation_semantic_object_frame(
9362
+ source_claims[claim_id], section_name, glossary
9363
+ ),
9364
  })
9365
 
9366
  confirmed = {}
 
9381
  return confirmed
9382
 
9383
  allowed_evidence = {
9384
+ 'CLAIM_MISSING','CLAIM_ADDED','ACTOR_CHANGED','RELATION_CHANGED','OBJECT_CHANGED',
9385
+ 'CONDITION_SCOPE_CHANGED','COMPARISON_DIRECTION_CHANGED','TEMPORAL_SCOPE_CHANGED',
9386
+ 'POLARITY_CHANGED','CAUSALITY_STRENGTH_CHANGED','MODALITY_UNCERTAINTY_CHANGED',
9387
+ 'CITATION_MOVED','ACRONYM_MISATTACHED',
9388
  'GRAMMATICAL_VIOLATION','CASE_GOVERNMENT','AGREEMENT','MORPHOLOGY','VERB_FORM',
9389
  'SYNTAX_STRUCTURE','PUNCTUATION','SYNTACTIC_COMPLETENESS','GRAMMAR_UNSPECIFIED',
9390
  'WORD_ORDER_UNGRAMMATICAL','WORD_ORDER_SEMANTIC_SCOPE_CHANGED',
 
9409
  },
9410
  'contamination_evidence_codes': list(row.get('contamination_evidence_codes') or [])[:2],
9411
  'grammar_evidence_codes': list(row.get('grammar_evidence_codes') or [])[:4],
9412
+ 'semantic_evidence_codes': list(row.get('semantic_evidence_codes') or [])[:6],
9413
+ 'semantic_object_deltas': list(row.get('semantic_object_deltas') or [])[:6],
9414
+ 'semantic_object': dict(row.get('semantic_object') or {}),
9415
  } for row in model_rows],
9416
  }
9417
  lexical_contract = self._translation_lexical_retention_contract(target_lang)
 
9420
  {lexical_contract}
9421
  For Russian, allow required case/number/gender inflection and retain sustained non-Russian prose as an additional contamination signal. For Kazakh, allow productive case, possessive, person/plural and vowel-harmony suffixes on the correct term stem, but treat clearly Russian lexical/syntactic substitution as blocking only when the pair itself supports that conclusion. For Chinese, allow natural reordering and sentence reshaping inside the same claim while preserving meaning.
9422
  For LANGUAGE_CONTAMINATION, deterministic_lexical is authoritative for ordinary English/Latin residue: when confirmed=false and token_count=0, you MUST NOT confirm UNTRANSLATED_LEXEME or UNTRANSLATED_PROSE. For Kazakh, a non-Latin contamination blocker may be confirmed only when contamination_evidence_codes contains KK_RUSSIAN_LEXICAL_INTRUSION or KK_RUSSIAN_SYNTACTIC_INTRUSION, and the evidence_code must match one of those supplied typed codes. The per-item glossary lists the frozen target terms that are legal for that source claim.
9423
+ For semantic issues, semantic_object is immutable source authority and semantic_evidence_codes/semantic_object_deltas are the only admissible semantic defect lineage. A generic claim that meaning changed is NOT sufficient. For SEMANTIC_DRIFT, confirm blocking only when the evidence_code exactly matches one of the supplied object-bound semantic dimensions. If the prior semantic object evidence is absent, invalid, or the current pair no longer supports the same dimension, return NOT_CONFIRMED. For NEGATION_DRIFT, CAUSALITY_STRENGTH_DRIFT, UNCERTAINTY_DRIFT, OMITTED_CLAIM, and ADDED_CLAIM, evidence_code must remain continuous with the corresponding supplied semantic_evidence_codes.
9424
  For GRAMMAR, grammar_evidence_codes is the typed defect history and must remain continuous into this final decision. Never use generic GRAMMATICAL_VIOLATION as final blocking evidence when a typed grammar history exists. If WORD_ORDER is supplied, non-canonical or stylistically different ordering is NOT blocking. Confirm only when the target itself is ungrammatical because of ordering (WORD_ORDER_UNGRAMMATICAL) or when ordering changes modifier/negation/modality/condition/relation scope relative to the English claim (WORD_ORDER_SEMANTIC_SCOPE_CHANGED). For CASE_GOVERNMENT, AGREEMENT, MORPHOLOGY, VERB_FORM, SYNTAX_STRUCTURE, PUNCTUATION, or SYNTACTIC_COMPLETENESS, a blocking evidence_code must match the supplied typed class. If only GRAMMAR_UNSPECIFIED was supplied, you may narrow it to one of those specific classes or one of the two WORD_ORDER consequence codes; do not confirm a generic grammar label.
9425
  Return valid JSON only: {{"items":[{{"id":"SP-P000001-C000001","issue_code":"GRAMMAR","decision":"CONFIRMED_BLOCKING","evidence_code":"AGREEMENT"}}]}}.
9426
  Decision must be CONFIRMED_BLOCKING or NOT_CONFIRMED. evidence_code must be one of: {sorted(allowed_evidence)}. Return every supplied id+issue_code pair exactly once."""
 
9449
  evidence_code = str(item.get('evidence_code') or '').strip().upper()
9450
  if decision not in {'CONFIRMED_BLOCKING', 'NOT_CONFIRMED'} or evidence_code not in allowed_evidence:
9451
  raise DeepSeekRequestError(f'Translation QA adjudication returned invalid bounded decision for {row["id"]}.')
9452
+ if row['code'] in {
9453
+ 'SEMANTIC_DRIFT','NEGATION_DRIFT','CAUSALITY_STRENGTH_DRIFT',
9454
+ 'UNCERTAINTY_DRIFT','OMITTED_CLAIM','ADDED_CLAIM',
9455
+ } and decision == 'CONFIRMED_BLOCKING':
9456
+ typed = {
9457
+ str(code or '').strip().upper()
9458
+ for code in (row.get('semantic_evidence_codes') or []) if str(code or '').strip()
9459
+ }
9460
+ if not typed or evidence_code not in typed:
9461
+ decision = 'NOT_CONFIRMED'
9462
+ evidence_code = 'QA_DISAGREEMENT'
9463
+ elif row['code'] == 'SEMANTIC_DRIFT':
9464
+ object_dims = {
9465
+ str(item.get('dimension') or '').strip().upper()
9466
+ for item in (row.get('semantic_object_deltas') or []) if isinstance(item, dict)
9467
+ }
9468
+ if evidence_code not in object_dims:
9469
+ decision = 'NOT_CONFIRMED'
9470
+ evidence_code = 'QA_DISAGREEMENT'
9471
  if row['code'] == 'LANGUAGE_CONTAMINATION':
9472
  lexical = dict(row.get('lexical_evidence') or {})
9473
  typed = set(row.get('contamination_evidence_codes') or [])
 
9847
  semantic_deltas.append(fallback)
9848
  if semantic_deltas:
9849
  repair_evidence['semantic_delta_codes'] = list(dict.fromkeys(semantic_deltas))[:6]
9850
+ specific_semantic = list(dict.fromkeys(semantic_deltas))[:6]
9851
+ # R6-R9: generic semantic drift cannot arrive here without
9852
+ # a source/target-bound semantic object. Specific semantic
9853
+ # codes (polarity/causality/modality/claim add-missing) are
9854
+ # derived directly above and do not require a second free
9855
+ # semantic-decomposition model call.
9856
+ if semantic_issue and not specific_semantic:
9857
+ repair_evidence['semantic_delta_codes'] = ['UNRESOLVED_SEMANTIC_DELTA']
9858
  if repair_evidence.get('semantic_delta_codes'):
9859
  self._record_translation_assurance(
9860
  'SEMANTIC_REPAIR_EVIDENCE', target_lang=target_lang,