S0-R1-C5.5-R6-R6 Canonical Glossary Reviewer Authority Closure

#94
Files changed (1) hide show
  1. core/pipeline_agent.py +174 -52
core/pipeline_agent.py CHANGED
@@ -74,7 +74,7 @@ class SSciPipelineAgent:
74
  MAX_TRANSLATION_CLAIM_REPAIR_ATTEMPTS = 2
75
  MAX_TRANSLATION_CLAIM_TOTAL_REPAIRS = 6
76
  PROTECTED_ENTITY_LEDGER_VERSION = 'protected-entity-ledger-v2'
77
- CANONICAL_GLOSSARY_QUALITY_VERSION = 'canonical-glossary-quality-v2'
78
  SEMANTIC_CLAIM_FRAME_VERSION = 'smartpaper-core-semantic-claim-frame-v1'
79
  CITATION_CLAIM_FRAME_VERSION = 'smartpaper-citation-claim-frame-v1'
80
 
@@ -7242,16 +7242,73 @@ TEXT:
7242
  # reduplication is left alone because it can be grammatical Chinese.
7243
  if re.search(r'([\u4e00-\u9fff]{2,4})\1', compact):
7244
  return True
 
 
 
 
 
 
 
 
 
 
 
7245
  return False
7246
 
7247
- def validate_and_refine_canonical_concept_glossary(self, concept_ledger, target_lang, glossary):
7248
- """Independently review strict canonical terms once before body translation.
 
7249
 
7250
- The first glossary call establishes candidate identities. This second,
7251
- bounded call verifies semantic identity and natural academic surface form
7252
- before those candidates become immutable for all later ZH/RU/KK sections.
7253
- At most one corrected target is accepted for each strict construct; no
7254
- section-level repair loop is involved.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7255
  """
7256
  mapping = dict(glossary or {})
7257
  rows = (dict(concept_ledger or {}).get('items') or []) if isinstance(concept_ledger, dict) else []
@@ -7271,67 +7328,132 @@ TEXT:
7271
  {'source': source, 'candidate_target': str(mapping.get(source) or '').strip()}
7272
  for source in strict_sources
7273
  ]
7274
- if any(not row['candidate_target'] for row in payload):
7275
- raise DeepSeekRequestError('Canonical glossary quality review received an empty strict target.')
 
 
 
 
 
 
 
 
 
 
7276
  language_rule = self._translation_language_rule(target_lang)
7277
- response = self.call_deepseek(
7278
- (
7279
- f"Independently review strict research-construct translations for {target_lang}. {language_rule} "
7280
- "Judge semantic identity and natural academic terminology, not stylistic preference. "
7281
- "Reject duplicated/malformed wording, concept narrowing/broadening, or a target that changes the construct. "
7282
- "Every protected exact source anchor inside a construct (for example AI, LLM, model/tool IDs, hypothesis IDs, formulas, statistics or exact numbers) must remain byte-for-byte in the target canonical label; never localize, delete, duplicate, or rename such an anchor. "
7283
- "If the candidate is sound, return it unchanged with accepted=true. If it is materially flawed, return exactly one corrected canonical target with accepted=false. "
7284
- "Do not merge distinct constructs. Return JSON only as "
7285
- '{"terms":[{"source":"...","candidate_target":"...","accepted":true,"target":"..."}]}'
7286
- " and return every source exactly once."
7287
- ),
7288
- json.dumps({'terms': payload}, ensure_ascii=False),
7289
- max_tokens=max(1200, min(4200, 180 + len(payload) * 120)),
7290
- temp=0.0, model_role='translation', thinking=False, allow_fallback=False,
7291
- )
7292
- parsed = self._extract_json_from_text(response)
7293
- items = parsed.get('terms', []) if isinstance(parsed, dict) else []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7294
  returned = {}
7295
  for item in items:
7296
  if not isinstance(item, dict):
7297
  continue
7298
  source = re.sub(r'\s+', ' ', str(item.get('source') or '')).strip()
7299
- if source:
7300
  returned[source] = item
7301
- if set(returned) != set(strict_sources):
7302
- raise DeepSeekRequestError('Canonical glossary quality review omitted or changed a strict source identity.')
 
 
 
 
 
 
 
7303
 
7304
  refined = dict(mapping)
7305
  for source in strict_sources:
7306
- item = returned[source]
7307
  candidate = str(mapping.get(source) or '').strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7308
  accepted_raw = item.get('accepted')
7309
  accepted = accepted_raw is True or str(accepted_raw).strip().casefold() == 'true'
7310
  reviewed_target = re.sub(r'\s+', ' ', str(item.get('target') or '')).strip()
7311
- suspicious = (
7312
- self._canonical_target_surface_suspicious(candidate, target_lang)
7313
- or not self._canonical_target_preserves_source_anchors(source, candidate)
7314
  )
7315
- if accepted and not suspicious:
7316
- # An acceptance cannot silently remap the frozen candidate.
7317
- if reviewed_target and reviewed_target != candidate:
7318
- raise DeepSeekRequestError('Canonical glossary review changed an accepted strict target.')
7319
- continue
7320
- if not reviewed_target or reviewed_target == candidate:
 
7321
  raise DeepSeekRequestError(
7322
- f'Canonical concept quality review did not provide a usable correction: {source[:80]}'
7323
  )
7324
- if reviewed_target.casefold() == source.casefold() and re.search(r'[a-z]{4,}', source):
7325
- raise DeepSeekRequestError(f'Canonical concept remained untranslated after quality review: {source[:80]}')
7326
- if (
7327
- self._canonical_target_surface_suspicious(reviewed_target, target_lang)
7328
- or not self._canonical_target_preserves_source_anchors(source, reviewed_target)
7329
- ):
7330
- raise DeepSeekRequestError(f'Canonical concept correction remains malformed: {source[:80]}')
7331
- refined[source] = reviewed_target
7332
- self._record_translation_assurance(
7333
- 'CANONICAL_GLOSSARY_REFINED', target_lang=target_lang, severity='repair',
7334
- decision='REPAIR_LOCAL', detail={'source': source[:120]},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7335
  )
7336
  return refined
7337
 
 
74
  MAX_TRANSLATION_CLAIM_REPAIR_ATTEMPTS = 2
75
  MAX_TRANSLATION_CLAIM_TOTAL_REPAIRS = 6
76
  PROTECTED_ENTITY_LEDGER_VERSION = 'protected-entity-ledger-v2'
77
+ CANONICAL_GLOSSARY_QUALITY_VERSION = 'canonical-glossary-quality-v3'
78
  SEMANTIC_CLAIM_FRAME_VERSION = 'smartpaper-core-semantic-claim-frame-v1'
79
  CITATION_CLAIM_FRAME_VERSION = 'smartpaper-citation-claim-frame-v1'
80
 
 
7242
  # reduplication is left alone because it can be grammatical Chinese.
7243
  if re.search(r'([\u4e00-\u9fff]{2,4})\1', compact):
7244
  return True
7245
+ # Canonical labels are short noun phrases. Immediate repetition of the
7246
+ # same substantive token is therefore a high-confidence malformed
7247
+ # surface in Russian/Kazakh (and remains safe for embedded acronyms).
7248
+ words = [
7249
+ token.casefold() for token in re.findall(
7250
+ r"[A-Za-zА-Яа-яЁёӘәҒғҚқҢңӨөҰұҮүҺһІі][A-Za-zА-Яа-яЁёӘәҒғҚқҢңӨөҰұҮүҺһІі'’\-]{2,}",
7251
+ value,
7252
+ )
7253
+ ]
7254
+ if any(left == right for left, right in zip(words, words[1:])):
7255
+ return True
7256
  return False
7257
 
7258
+ @classmethod
7259
+ def _canonical_target_deterministic_defects(cls, source, target, target_lang):
7260
+ """Return only deterministic defects that make a canonical label unsafe to freeze.
7261
 
7262
+ The model-based quality reviewer is secondary evidence. It may improve a
7263
+ safe candidate, but it must never turn an otherwise deterministic-safe
7264
+ label into a task-fatal error merely because the review output is
7265
+ contradictory, incomplete, or non-actionable.
7266
+ """
7267
+ source_value = re.sub(r'\s+', ' ', str(source or '')).strip()
7268
+ target_value = re.sub(r'\s+', ' ', str(target or '')).strip()
7269
+ defects = []
7270
+ if not target_value:
7271
+ return ['EMPTY_TARGET']
7272
+
7273
+ compact_source = re.sub(r'[^A-Za-z0-9]', '', source_value)
7274
+ pure_technical_source = bool(compact_source) and ' ' not in source_value and (
7275
+ compact_source.isupper()
7276
+ or any(ch.isdigit() for ch in compact_source)
7277
+ or (len(compact_source) > 2 and any(ch.isupper() for ch in compact_source[1:]))
7278
+ )
7279
+ if not pure_technical_source:
7280
+ if target_value.casefold() == source_value.casefold() and re.search(r'[a-z]{4,}', source_value):
7281
+ defects.append('UNTRANSLATED')
7282
+ language_code = cls._translation_language_profile(target_lang).get('language_code')
7283
+ if language_code == 'zh' and not re.search(r'[\u4e00-\u9fff]', target_value):
7284
+ defects.append('TARGET_LANGUAGE_SIGNAL_MISSING')
7285
+ if language_code in {'ru', 'kk'} and not re.search(r'[А-Яа-яЁёӘәҒғҚқҢңӨөҰұҮүҺһІі]', target_value):
7286
+ defects.append('TARGET_LANGUAGE_SIGNAL_MISSING')
7287
+
7288
+ if cls._canonical_target_surface_suspicious(target_value, target_lang):
7289
+ defects.append('MALFORMED_SURFACE')
7290
+ if not cls._canonical_target_preserves_source_anchors(source_value, target_value):
7291
+ defects.append('PROTECTED_ANCHOR_LOSS')
7292
+ return list(dict.fromkeys(defects))
7293
+
7294
+ def validate_and_refine_canonical_concept_glossary(self, concept_ledger, target_lang, glossary):
7295
+ """Review strict canonical terms without giving the reviewer unilateral veto power.
7296
+
7297
+ Contract:
7298
+ 1. Server-owned deterministic invariants decide whether a candidate is
7299
+ safe enough to freeze (non-empty, translated target-language signal,
7300
+ well-formed surface, protected-anchor preservation).
7301
+ 2. The independent model reviewer may replace a candidate only with one
7302
+ distinct, deterministic-safe correction.
7303
+ 3. If the reviewer rejects/omits/contradicts a candidate but supplies no
7304
+ usable correction, an already safe candidate is retained with an
7305
+ assurance warning instead of aborting the whole manuscript.
7306
+ 4. If the candidate itself is deterministic-unsafe and no safe correction
7307
+ exists, the gate remains fail-closed.
7308
+
7309
+ This prevents a secondary quality-review formatting/judgment failure from
7310
+ becoming a single point of failure across Chinese, Russian, and Kazakh
7311
+ translation while preserving genuine terminology-integrity barriers.
7312
  """
7313
  mapping = dict(glossary or {})
7314
  rows = (dict(concept_ledger or {}).get('items') or []) if isinstance(concept_ledger, dict) else []
 
7328
  {'source': source, 'candidate_target': str(mapping.get(source) or '').strip()}
7329
  for source in strict_sources
7330
  ]
7331
+ candidate_defects = {
7332
+ row['source']: self._canonical_target_deterministic_defects(
7333
+ row['source'], row['candidate_target'], target_lang
7334
+ )
7335
+ for row in payload
7336
+ }
7337
+ empty_sources = [source for source, defects in candidate_defects.items() if 'EMPTY_TARGET' in defects]
7338
+ if empty_sources:
7339
+ raise DeepSeekRequestError(
7340
+ 'Canonical glossary quality review received an empty strict target: ' + ', '.join(empty_sources[:3])
7341
+ )
7342
+
7343
  language_rule = self._translation_language_rule(target_lang)
7344
+ try:
7345
+ response = self.call_deepseek(
7346
+ (
7347
+ f"Independently review strict research-construct translations for {target_lang}. {language_rule} "
7348
+ "Judge semantic identity and natural academic terminology, not stylistic preference. "
7349
+ "Reject duplicated/malformed wording, concept narrowing/broadening, or a target that changes the construct. "
7350
+ "Every protected exact source anchor inside a construct (for example AI, LLM, model/tool IDs, hypothesis IDs, formulas, statistics or exact numbers) must remain byte-for-byte in the target canonical label; never localize, delete, duplicate, or rename such an anchor. "
7351
+ "If the candidate is sound, return it unchanged with accepted=true. If it is materially flawed, return exactly one corrected canonical target with accepted=false. "
7352
+ "Do not merge distinct constructs. Return JSON only as "
7353
+ '{"terms":[{"source":"...","candidate_target":"...","accepted":true,"target":"..."}]}'
7354
+ " and return every source exactly once."
7355
+ ),
7356
+ json.dumps({'terms': payload}, ensure_ascii=False),
7357
+ max_tokens=max(1200, min(4200, 180 + len(payload) * 120)),
7358
+ temp=0.0, model_role='translation', thinking=False, allow_fallback=False,
7359
+ )
7360
+ parsed = self._extract_json_from_text(response)
7361
+ items = parsed.get('terms', []) if isinstance(parsed, dict) else []
7362
+ except DeepSeekRequestError as exc:
7363
+ unsafe = [source for source, defects in candidate_defects.items() if defects]
7364
+ if unsafe:
7365
+ source = unsafe[0]
7366
+ raise DeepSeekRequestError(
7367
+ f'Canonical concept has no deterministic-safe target after review failure: {source[:80]} '
7368
+ f'defects={candidate_defects[source]}'
7369
+ ) from exc
7370
+ self._record_translation_assurance(
7371
+ 'CANONICAL_GLOSSARY_REVIEW_UNAVAILABLE', target_lang=target_lang, severity='diagnostic',
7372
+ decision='PASS_WITH_WARNING', detail={'strict_sources': len(strict_sources)},
7373
+ )
7374
+ return mapping
7375
+
7376
  returned = {}
7377
  for item in items:
7378
  if not isinstance(item, dict):
7379
  continue
7380
  source = re.sub(r'\s+', ' ', str(item.get('source') or '')).strip()
7381
+ if source in strict_sources:
7382
  returned[source] = item
7383
+ extra_count = sum(
7384
+ 1 for item in items if isinstance(item, dict)
7385
+ and re.sub(r'\s+', ' ', str(item.get('source') or '')).strip() not in strict_sources
7386
+ )
7387
+ if extra_count:
7388
+ self._record_translation_assurance(
7389
+ 'CANONICAL_GLOSSARY_REVIEW_EXTRA_SOURCE', target_lang=target_lang, severity='diagnostic',
7390
+ decision='PASS_WITH_WARNING', detail={'extra_sources': extra_count},
7391
+ )
7392
 
7393
  refined = dict(mapping)
7394
  for source in strict_sources:
 
7395
  candidate = str(mapping.get(source) or '').strip()
7396
+ defects = list(candidate_defects.get(source) or [])
7397
+ item = returned.get(source)
7398
+ if not item:
7399
+ if defects:
7400
+ raise DeepSeekRequestError(
7401
+ f'Canonical concept has no deterministic-safe target after review omission: {source[:80]} '
7402
+ f'defects={defects}'
7403
+ )
7404
+ self._record_translation_assurance(
7405
+ 'CANONICAL_GLOSSARY_REVIEW_INCONCLUSIVE', target_lang=target_lang, severity='diagnostic',
7406
+ decision='PASS_WITH_WARNING', detail={'source': source[:120], 'reason': 'OMITTED_SOURCE'},
7407
+ )
7408
+ continue
7409
+
7410
  accepted_raw = item.get('accepted')
7411
  accepted = accepted_raw is True or str(accepted_raw).strip().casefold() == 'true'
7412
  reviewed_target = re.sub(r'\s+', ' ', str(item.get('target') or '')).strip()
7413
+ reviewed_defects = (
7414
+ self._canonical_target_deterministic_defects(source, reviewed_target, target_lang)
7415
+ if reviewed_target else ['EMPTY_TARGET']
7416
  )
7417
+
7418
+ # A sound acceptance means no remapping. If the reviewer claims to
7419
+ # accept but also changes the term, keep a safe frozen candidate; if
7420
+ # the candidate is unsafe, a distinct safe target may still rescue it.
7421
+ if accepted and reviewed_target in {'', candidate}:
7422
+ if not defects:
7423
+ continue
7424
  raise DeepSeekRequestError(
7425
+ f'Canonical concept accepted despite deterministic defects: {source[:80]} defects={defects}'
7426
  )
7427
+
7428
+ distinct_safe_correction = bool(
7429
+ reviewed_target and reviewed_target != candidate and not reviewed_defects
7430
+ )
7431
+ if distinct_safe_correction and (not accepted or defects):
7432
+ refined[source] = reviewed_target
7433
+ self._record_translation_assurance(
7434
+ 'CANONICAL_GLOSSARY_REFINED', target_lang=target_lang, severity='repair',
7435
+ decision='REPAIR_LOCAL', detail={'source': source[:120]},
7436
+ )
7437
+ continue
7438
+
7439
+ if not defects:
7440
+ # Reviewer vetoes, omits a correction, proposes an invalid
7441
+ # correction, or contradicts accepted=true. None of these is
7442
+ # actionable evidence against an already deterministic-safe term.
7443
+ reason = 'NON_ACTIONABLE_REJECTION'
7444
+ if accepted and reviewed_target and reviewed_target != candidate:
7445
+ reason = 'ACCEPTED_BUT_CHANGED'
7446
+ elif reviewed_target and reviewed_target != candidate and reviewed_defects:
7447
+ reason = 'INVALID_CORRECTION'
7448
+ self._record_translation_assurance(
7449
+ 'CANONICAL_GLOSSARY_REVIEW_INCONCLUSIVE', target_lang=target_lang, severity='diagnostic',
7450
+ decision='PASS_WITH_WARNING', detail={'source': source[:120], 'reason': reason},
7451
+ )
7452
+ continue
7453
+
7454
+ raise DeepSeekRequestError(
7455
+ f'Canonical concept has no deterministic-safe target after quality review: {source[:80]} '
7456
+ f'defects={defects}'
7457
  )
7458
  return refined
7459