S0-R1-C5.5-R6-R7 Canonical Glossary Protected-IR Compiler Closure

#95
Files changed (1) hide show
  1. core/pipeline_agent.py +341 -85
core/pipeline_agent.py CHANGED
@@ -74,7 +74,9 @@ 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-v3'
 
 
78
  SEMANTIC_CLAIM_FRAME_VERSION = 'smartpaper-core-semantic-claim-frame-v1'
79
  CITATION_CLAIM_FRAME_VERSION = 'smartpaper-citation-claim-frame-v1'
80
 
@@ -7163,8 +7165,224 @@ TEXT:
7163
  raise DeepSeekRequestError(f'Translation left unresolved protected placeholders in {block_id}.')
7164
  return text.strip() if keep_claim_markers else cls._strip_translation_claim_markers(text)
7165
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7166
  def seed_canonical_concept_glossary(self, concept_ledger, target_lang, existing=None):
7167
- """Translate/freeze canonical task concepts once before section work."""
 
 
 
 
 
7168
  mapping = dict(existing or {})
7169
  rows = (dict(concept_ledger or {}).get('items') or []) if isinstance(concept_ledger, dict) else []
7170
  sources = []
@@ -7184,45 +7402,52 @@ TEXT:
7184
  flexible_sources.append(term)
7185
  else:
7186
  strict_sources.append(term)
7187
- # Only true research constructs receive deterministic canonical-identity
7188
- # enforcement. Descriptive academic terms still receive a preferred
7189
- # glossary translation but may vary naturally with target-language
7190
- # grammar or equivalent wording; semantic QA remains authoritative.
7191
  self._canonical_concept_sources = set(strict_sources)
7192
  self._canonical_flexible_sources = set(flexible_sources)
7193
  self._canonical_concept_kinds = dict(source_kinds)
7194
  if not sources:
7195
  return mapping
7196
 
7197
- missing = [term for term in sources if not str(mapping.get(term) or '').strip()]
7198
- # Acronyms/technical identifiers preserve literal identity; conceptual
7199
- # phrases must be translated instead of becoming legal English residue.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7200
  translate_terms = []
7201
- for term in missing:
7202
  compact = re.sub(r'[^A-Za-z0-9]', '', term)
7203
- technical = bool(compact) and (compact.isupper() or any(ch.isdigit() for ch in compact) or (len(compact) > 2 and any(ch.isupper() for ch in compact[1:]))) and ' ' not in term
 
 
 
7204
  if technical:
7205
  mapping[term] = term
7206
  else:
7207
  translate_terms.append(term)
7208
  if translate_terms:
7209
- rule = self._translation_language_rule(target_lang)
7210
- response = self.call_deepseek(
7211
- f"Translate the supplied frozen research concepts into {target_lang}. {rule} Each source is a canonical concept identity, not a stylistic phrase. Give one stable canonical target term for each source; do not merge distinct sources or replace one concept with a near-synonym. Return JSON only as {{\"terms\":[{{\"source\":\"...\",\"target\":\"...\"}}]}} and return every source exactly once.",
7212
- json.dumps({'terms': translate_terms}, ensure_ascii=False),
7213
- max_tokens=max(1200, min(5000, 120 + len(translate_terms) * 90)),
7214
- temp=0.0, model_role='translation', thinking=False, allow_fallback=False,
7215
- )
7216
- parsed = self._extract_json_from_text(response)
7217
- items = parsed.get('terms', []) if isinstance(parsed, dict) else []
7218
- returned = {
7219
- re.sub(r'\s+', ' ', str(item.get('source') or '')).strip(): re.sub(r'\s+', ' ', str(item.get('target') or '')).strip()
7220
- for item in items if isinstance(item, dict)
7221
- }
7222
- if set(returned) != set(translate_terms) or any(not returned.get(term) for term in translate_terms):
7223
- raise DeepSeekRequestError('Canonical concept glossary identity mismatch or omitted term.')
7224
  for term in translate_terms:
7225
- target = returned[term]
 
 
7226
  if target.casefold() == term.casefold() and re.search(r'[a-z]{4,}', term):
7227
  raise DeepSeekRequestError(f'Canonical concept remained untranslated: {term[:80]}')
7228
  mapping[term] = target
@@ -7287,28 +7512,15 @@ TEXT:
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 []
@@ -7324,16 +7536,47 @@ TEXT:
7324
  if not strict_sources:
7325
  return mapping
7326
 
7327
- payload = [
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(
@@ -7344,16 +7587,16 @@ TEXT:
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
  )
@@ -7377,12 +7620,12 @@ TEXT:
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(
@@ -7392,9 +7635,19 @@ TEXT:
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(
@@ -7409,16 +7662,22 @@ TEXT:
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(
@@ -7437,13 +7696,10 @@ TEXT:
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',
 
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-v4'
78
+ CANONICAL_GLOSSARY_IR_VERSION = 'canonical-glossary-protected-ir-v1'
79
+ MAX_CANONICAL_GLOSSARY_IR_ATTEMPTS = 2
80
  SEMANTIC_CLAIM_FRAME_VERSION = 'smartpaper-core-semantic-claim-frame-v1'
81
  CITATION_CLAIM_FRAME_VERSION = 'smartpaper-citation-claim-frame-v1'
82
 
 
7165
  raise DeepSeekRequestError(f'Translation left unresolved protected placeholders in {block_id}.')
7166
  return text.strip() if keep_claim_markers else cls._strip_translation_claim_markers(text)
7167
 
7168
+ @classmethod
7169
+ def _compile_canonical_glossary_ir(cls, source):
7170
+ """Compile one canonical concept into a protected, server-owned IR.
7171
+
7172
+ Exact technical identities are replaced by opaque SP-G placeholders before
7173
+ any model sees the concept. The model may translate only the linguistic
7174
+ surface around those placeholders; exact anchor values remain server-side.
7175
+ """
7176
+ source_value = re.sub(r'\s+', ' ', str(source or '')).strip()
7177
+ mapping = {}
7178
+ counter = 0
7179
+ pattern = cls._translation_protected_pattern()
7180
+
7181
+ def replace(match):
7182
+ nonlocal counter
7183
+ original = match.group(0)
7184
+ anchor_type = cls._translation_anchor_type(original)
7185
+ if cls._translation_fact_render_policy(anchor_type) != 'exact':
7186
+ return original
7187
+ counter += 1
7188
+ placeholder = f'[[SP-G{counter:06d}]]'
7189
+ mapping[placeholder] = {
7190
+ 'source': original,
7191
+ 'type': anchor_type,
7192
+ 'render_policy': 'exact',
7193
+ }
7194
+ return placeholder
7195
+
7196
+ protected_source = pattern.sub(replace, source_value)
7197
+ return {
7198
+ 'ir_version': cls.CANONICAL_GLOSSARY_IR_VERSION,
7199
+ 'source': source_value,
7200
+ 'protected_source': protected_source,
7201
+ 'anchors': mapping,
7202
+ }
7203
+
7204
+ @staticmethod
7205
+ def _canonical_glossary_ir_placeholders(value):
7206
+ return re.findall(r'\[\[SP-G\d{6}\]\]', str(value or ''))
7207
+
7208
+ @classmethod
7209
+ def _validate_canonical_glossary_ir_target(cls, protected_target, ir, block_id=''):
7210
+ """Fail closed on deleted, duplicated, invented, or reordered SP-G anchors."""
7211
+ text = re.sub(r'\s+', ' ', str(protected_target or '')).strip()
7212
+ expected = list(dict(ir or {}).get('anchors', {}))
7213
+ found = cls._canonical_glossary_ir_placeholders(text)
7214
+ if Counter(found) != Counter(expected):
7215
+ missing = list((Counter(expected) - Counter(found)).elements())[:8]
7216
+ added = list((Counter(found) - Counter(expected)).elements())[:8]
7217
+ raise DeepSeekRequestError(
7218
+ f'Canonical glossary protected IR placeholder mismatch in {block_id}: '
7219
+ f'missing={missing}, added={added}.'
7220
+ )
7221
+ if found != expected:
7222
+ raise DeepSeekRequestError(
7223
+ f'Canonical glossary protected IR anchor order changed in {block_id}: '
7224
+ f'expected={expected}, got={found}.'
7225
+ )
7226
+ return True
7227
+
7228
+ @classmethod
7229
+ def _restore_canonical_glossary_ir_target(cls, protected_target, ir, block_id=''):
7230
+ cls._validate_canonical_glossary_ir_target(protected_target, ir, block_id=block_id)
7231
+ text = re.sub(r'\s+', ' ', str(protected_target or '')).strip()
7232
+ for placeholder, manifest in dict(dict(ir or {}).get('anchors', {})).items():
7233
+ text = text.replace(placeholder, str(manifest.get('source') or ''))
7234
+ if re.search(r'\[\[SP-G\d{6}\]\]', text):
7235
+ raise DeepSeekRequestError(
7236
+ f'Canonical glossary protected IR left unresolved placeholders in {block_id}.'
7237
+ )
7238
+ return text.strip()
7239
+
7240
+ @classmethod
7241
+ def _protect_existing_canonical_target_with_ir(cls, target, ir, block_id=''):
7242
+ """Project a sealed target back into SP-G form for secondary review.
7243
+
7244
+ This is used only after deterministic anchor checks pass. Reviewers never
7245
+ receive the raw exact anchor values and therefore cannot translate or rename
7246
+ them; they may only edit the surrounding linguistic surface.
7247
+ """
7248
+ text = re.sub(r'\s+', ' ', str(target or '')).strip()
7249
+ for placeholder, manifest in dict(dict(ir or {}).get('anchors', {})).items():
7250
+ surface = str(manifest.get('source') or '')
7251
+ if not surface:
7252
+ continue
7253
+ pos = text.find(surface)
7254
+ if pos < 0:
7255
+ raise DeepSeekRequestError(
7256
+ f'Canonical glossary target cannot be projected to protected IR in {block_id}: '
7257
+ f'missing exact anchor type={manifest.get("type") or "IMMUTABLE"}.'
7258
+ )
7259
+ text = text[:pos] + placeholder + text[pos + len(surface):]
7260
+ cls._validate_canonical_glossary_ir_target(text, ir, block_id=block_id)
7261
+ return text
7262
+
7263
+ @classmethod
7264
+ def _canonical_exact_anchor_sequence(cls, value):
7265
+ """Return exact-render technical anchors as a byte-stable sequence."""
7266
+ _protected, mapping = cls._protect_translation_tokens(str(value or ''))
7267
+ return [
7268
+ str(manifest.get('source') or '')
7269
+ for manifest in dict(mapping or {}).values()
7270
+ if isinstance(manifest, dict)
7271
+ and str(manifest.get('render_policy') or '') == 'exact'
7272
+ and str(manifest.get('source') or '') not in {'', '|'}
7273
+ ]
7274
+
7275
+ @classmethod
7276
+ def _canonical_anchor_drift_defects(cls, source, target):
7277
+ expected = cls._canonical_exact_anchor_sequence(source)
7278
+ actual = cls._canonical_exact_anchor_sequence(target)
7279
+ expected_counts = Counter(expected)
7280
+ actual_counts = Counter(actual)
7281
+ defects = []
7282
+ if any(actual_counts.get(anchor, 0) < count for anchor, count in expected_counts.items()):
7283
+ defects.append('PROTECTED_ANCHOR_LOSS')
7284
+ if any(actual_counts.get(anchor, 0) > count for anchor, count in expected_counts.items()):
7285
+ defects.append('PROTECTED_ANCHOR_DUPLICATION')
7286
+ # Exact target-side technical identities that were not source-owned are
7287
+ # not allowed to appear inside a strict canonical identity.
7288
+ if any(anchor not in expected_counts for anchor in actual_counts):
7289
+ defects.append('PROTECTED_ANCHOR_ADDITION')
7290
+ if expected_counts == actual_counts and expected != actual:
7291
+ defects.append('PROTECTED_ANCHOR_ORDER_DRIFT')
7292
+ return defects
7293
+
7294
+ def _translate_canonical_glossary_ir_terms(self, terms, target_lang):
7295
+ """Translate canonical concepts through protected IR with one bounded skeleton retry."""
7296
+ terms = list(terms or [])
7297
+ if not terms:
7298
+ return {}
7299
+ ir_by_id = {}
7300
+ source_by_id = {}
7301
+ for index, source in enumerate(terms, start=1):
7302
+ term_id = f'G{index:06d}'
7303
+ ir_by_id[term_id] = self._compile_canonical_glossary_ir(source)
7304
+ source_by_id[term_id] = source
7305
+
7306
+ resolved = {}
7307
+ pending = list(ir_by_id)
7308
+ rule = self._translation_language_rule(target_lang)
7309
+ for attempt in range(1, self.MAX_CANONICAL_GLOSSARY_IR_ATTEMPTS + 1):
7310
+ if not pending:
7311
+ break
7312
+ payload = []
7313
+ for term_id in pending:
7314
+ ir = ir_by_id[term_id]
7315
+ payload.append({
7316
+ 'term_id': term_id,
7317
+ 'protected_source': ir['protected_source'],
7318
+ 'anchor_manifest': [
7319
+ {'id': placeholder, 'type': str(manifest.get('type') or 'IMMUTABLE')}
7320
+ for placeholder, manifest in ir['anchors'].items()
7321
+ ],
7322
+ })
7323
+ strict = (
7324
+ ' This is a strict skeleton retry. Copy every [[SP-Gxxxxxx]] placeholder byte-for-byte, '
7325
+ 'exactly once, and in the same order. Do not translate, delete, duplicate, rename, or move placeholders.'
7326
+ if attempt > 1 else ''
7327
+ )
7328
+ response = self.call_deepseek(
7329
+ (
7330
+ f'Translate the supplied protected research-concept surfaces into {target_lang}. {rule} '
7331
+ 'SP-G placeholders are opaque server-owned technical identities: preserve every placeholder '
7332
+ 'byte-for-byte, exactly once, and in the same order; translate only the linguistic surface around them. '
7333
+ 'Do not infer or spell out what a placeholder represents. Return JSON only as '
7334
+ '{"terms":[{"term_id":"G000001","target":"..."}]} and return every supplied term_id exactly once.'
7335
+ + strict
7336
+ ),
7337
+ json.dumps({'ir_version': self.CANONICAL_GLOSSARY_IR_VERSION, 'terms': payload}, ensure_ascii=False),
7338
+ max_tokens=max(1200, min(5000, 120 + len(payload) * 90)),
7339
+ temp=0.0, model_role='translation', thinking=False, allow_fallback=False,
7340
+ )
7341
+ parsed = self._extract_json_from_text(response)
7342
+ items = parsed.get('terms', []) if isinstance(parsed, dict) else []
7343
+ returned = {
7344
+ str(item.get('term_id') or '').strip(): re.sub(r'\s+', ' ', str(item.get('target') or '')).strip()
7345
+ for item in items if isinstance(item, dict)
7346
+ }
7347
+ next_pending = []
7348
+ for term_id in pending:
7349
+ protected_target = returned.get(term_id, '')
7350
+ try:
7351
+ if not protected_target:
7352
+ raise DeepSeekRequestError(
7353
+ f'Canonical glossary protected IR omitted term_id={term_id}.'
7354
+ )
7355
+ restored = self._restore_canonical_glossary_ir_target(
7356
+ protected_target, ir_by_id[term_id], block_id=f'canonical:{term_id}'
7357
+ )
7358
+ defects = self._canonical_target_deterministic_defects(
7359
+ source_by_id[term_id], restored, target_lang
7360
+ )
7361
+ anchor_defects = [d for d in defects if d.startswith('PROTECTED_ANCHOR_')]
7362
+ if anchor_defects:
7363
+ raise DeepSeekRequestError(
7364
+ f'Canonical glossary protected IR restoration violated anchor contract: '
7365
+ f'{term_id} defects={anchor_defects}'
7366
+ )
7367
+ resolved[source_by_id[term_id]] = restored
7368
+ except DeepSeekRequestError:
7369
+ if attempt >= self.MAX_CANONICAL_GLOSSARY_IR_ATTEMPTS:
7370
+ raise
7371
+ next_pending.append(term_id)
7372
+ pending = next_pending
7373
+ if pending:
7374
+ raise DeepSeekRequestError(
7375
+ 'Canonical glossary protected IR exhausted with unresolved terms: ' + ', '.join(pending[:3])
7376
+ )
7377
+ return resolved
7378
+
7379
  def seed_canonical_concept_glossary(self, concept_ledger, target_lang, existing=None):
7380
+ """Translate/freeze canonical task concepts through protected IR.
7381
+
7382
+ R6-R7 moves exact technical identities out of model ownership. Existing
7383
+ safe glossary entries remain frozen; missing or deterministic-unsafe
7384
+ entries are rebuilt through SP-G protected IR before quality review.
7385
+ """
7386
  mapping = dict(existing or {})
7387
  rows = (dict(concept_ledger or {}).get('items') or []) if isinstance(concept_ledger, dict) else []
7388
  sources = []
 
7402
  flexible_sources.append(term)
7403
  else:
7404
  strict_sources.append(term)
 
 
 
 
7405
  self._canonical_concept_sources = set(strict_sources)
7406
  self._canonical_flexible_sources = set(flexible_sources)
7407
  self._canonical_concept_kinds = dict(source_kinds)
7408
  if not sources:
7409
  return mapping
7410
 
7411
+ # Rebuild only entries that are missing or whose server-owned identity
7412
+ # invariants are already broken. Safe historical targets remain frozen.
7413
+ rebuild = []
7414
+ for term in sources:
7415
+ current = re.sub(r'\s+', ' ', str(mapping.get(term) or '')).strip()
7416
+ if not current:
7417
+ rebuild.append(term)
7418
+ continue
7419
+ if term in strict_sources:
7420
+ defects = self._canonical_target_deterministic_defects(term, current, target_lang)
7421
+ if defects:
7422
+ rebuild.append(term)
7423
+ mapping.pop(term, None)
7424
+ self._record_translation_assurance(
7425
+ 'CANONICAL_GLOSSARY_STALE_TARGET_REBUILD', target_lang=target_lang,
7426
+ severity='repair', decision='REPAIR_LOCAL',
7427
+ detail={'source': term[:120], 'defects': ','.join(defects[:4])},
7428
+ )
7429
+ continue
7430
+ elif self._canonical_anchor_drift_defects(term, current):
7431
+ rebuild.append(term)
7432
+ mapping.pop(term, None)
7433
+
7434
  translate_terms = []
7435
+ for term in rebuild:
7436
  compact = re.sub(r'[^A-Za-z0-9]', '', term)
7437
+ technical = bool(compact) and (
7438
+ compact.isupper() or any(ch.isdigit() for ch in compact)
7439
+ or (len(compact) > 2 and any(ch.isupper() for ch in compact[1:]))
7440
+ ) and ' ' not in term
7441
  if technical:
7442
  mapping[term] = term
7443
  else:
7444
  translate_terms.append(term)
7445
  if translate_terms:
7446
+ translated = self._translate_canonical_glossary_ir_terms(translate_terms, target_lang)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7447
  for term in translate_terms:
7448
+ target = re.sub(r'\s+', ' ', str(translated.get(term) or '')).strip()
7449
+ if not target:
7450
+ raise DeepSeekRequestError(f'Canonical concept glossary omitted term after protected IR: {term[:80]}')
7451
  if target.casefold() == term.casefold() and re.search(r'[a-z]{4,}', term):
7452
  raise DeepSeekRequestError(f'Canonical concept remained untranslated: {term[:80]}')
7453
  mapping[term] = target
 
7512
 
7513
  if cls._canonical_target_surface_suspicious(target_value, target_lang):
7514
  defects.append('MALFORMED_SURFACE')
7515
+ defects.extend(cls._canonical_anchor_drift_defects(source_value, target_value))
 
7516
  return list(dict.fromkeys(defects))
7517
 
7518
  def validate_and_refine_canonical_concept_glossary(self, concept_ledger, target_lang, glossary):
7519
+ """Review strict canonical terms through the same protected IR contract.
7520
+
7521
+ The secondary reviewer never receives raw exact anchor values. It may
7522
+ only judge/repair the linguistic surface around opaque SP-G placeholders.
7523
+ Server-side deterministic invariants remain the final authority.
 
 
 
 
 
 
 
 
 
 
 
 
7524
  """
7525
  mapping = dict(glossary or {})
7526
  rows = (dict(concept_ledger or {}).get('items') or []) if isinstance(concept_ledger, dict) else []
 
7536
  if not strict_sources:
7537
  return mapping
7538
 
7539
+ source_by_id = {}
7540
+ id_by_source = {}
7541
+ ir_by_source = {}
7542
+ payload = []
7543
+ candidate_defects = {}
7544
+ protected_candidates = {}
7545
+ for index, source in enumerate(strict_sources, start=1):
7546
+ term_id = f'G{index:06d}'
7547
+ source_by_id[term_id] = source
7548
+ id_by_source[source] = term_id
7549
+ ir = self._compile_canonical_glossary_ir(source)
7550
+ ir_by_source[source] = ir
7551
+ candidate = re.sub(r'\s+', ' ', str(mapping.get(source) or '')).strip()
7552
+ defects = self._canonical_target_deterministic_defects(source, candidate, target_lang)
7553
+ candidate_defects[source] = defects
7554
+ protected_candidate = ''
7555
+ if candidate and not any(code.startswith('PROTECTED_ANCHOR_') for code in defects):
7556
+ try:
7557
+ protected_candidate = self._protect_existing_canonical_target_with_ir(
7558
+ candidate, ir, block_id=f'canonical-review:{term_id}'
7559
+ )
7560
+ except DeepSeekRequestError:
7561
+ # Treat projection failure as an anchor-integrity defect. In
7562
+ # normal production R6-R7 seed rebuilds such a stale target
7563
+ # before review; direct callers remain fail-closed unless the
7564
+ # reviewer supplies a fresh protected correction.
7565
+ if 'PROTECTED_ANCHOR_LOSS' not in defects:
7566
+ defects = list(defects) + ['PROTECTED_ANCHOR_LOSS']
7567
+ candidate_defects[source] = list(dict.fromkeys(defects))
7568
+ protected_candidates[source] = protected_candidate
7569
+ payload.append({
7570
+ 'term_id': term_id,
7571
+ 'protected_source': ir['protected_source'],
7572
+ 'candidate_target': protected_candidate,
7573
+ 'deterministic_defects': list(candidate_defects[source]),
7574
+ 'anchor_manifest': [
7575
+ {'id': placeholder, 'type': str(manifest.get('type') or 'IMMUTABLE')}
7576
+ for placeholder, manifest in ir['anchors'].items()
7577
+ ],
7578
+ })
7579
+
7580
  empty_sources = [source for source, defects in candidate_defects.items() if 'EMPTY_TARGET' in defects]
7581
  if empty_sources:
7582
  raise DeepSeekRequestError(
 
7587
  try:
7588
  response = self.call_deepseek(
7589
  (
7590
+ f'Independently review strict research-construct translations for {target_lang}. {language_rule} '
7591
+ 'Judge semantic identity and natural academic terminology, not stylistic preference. '
7592
+ 'SP-G placeholders are opaque server-owned technical identities. Never infer, translate, delete, duplicate, '
7593
+ 'rename, or reorder them. Preserve every placeholder byte-for-byte, exactly once, and in the same order. '
7594
+ 'If candidate_target is empty, construct one safe target from protected_source. If the candidate is sound, '
7595
+ 'return it unchanged with accepted=true. If it is materially flawed, return exactly one corrected protected '
7596
+ 'target with accepted=false. Do not merge distinct constructs. Return JSON only as '
7597
+ '{"terms":[{"term_id":"G000001","accepted":true,"target":"..."}]} and return every term_id exactly once.'
7598
  ),
7599
+ json.dumps({'ir_version': self.CANONICAL_GLOSSARY_IR_VERSION, 'terms': payload}, ensure_ascii=False),
7600
  max_tokens=max(1200, min(4200, 180 + len(payload) * 120)),
7601
  temp=0.0, model_role='translation', thinking=False, allow_fallback=False,
7602
  )
 
7620
  for item in items:
7621
  if not isinstance(item, dict):
7622
  continue
7623
+ term_id = str(item.get('term_id') or '').strip()
7624
+ if term_id in source_by_id:
7625
+ returned[term_id] = item
7626
  extra_count = sum(
7627
  1 for item in items if isinstance(item, dict)
7628
+ and str(item.get('term_id') or '').strip() not in source_by_id
7629
  )
7630
  if extra_count:
7631
  self._record_translation_assurance(
 
7635
 
7636
  refined = dict(mapping)
7637
  for source in strict_sources:
7638
+ term_id = id_by_source[source]
7639
+ candidate = re.sub(r'\s+', ' ', str(mapping.get(source) or '')).strip()
7640
  defects = list(candidate_defects.get(source) or [])
7641
+ protected_candidate = str(protected_candidates.get(source) or '')
7642
+ # Populate lazily because a candidate may have been anchor-unsafe.
7643
+ if not protected_candidate and candidate and not any(code.startswith('PROTECTED_ANCHOR_') for code in defects):
7644
+ try:
7645
+ protected_candidate = self._protect_existing_canonical_target_with_ir(
7646
+ candidate, ir_by_source[source], block_id=f'canonical-review:{term_id}'
7647
+ )
7648
+ except DeepSeekRequestError:
7649
+ protected_candidate = ''
7650
+ item = returned.get(term_id)
7651
  if not item:
7652
  if defects:
7653
  raise DeepSeekRequestError(
 
7662
 
7663
  accepted_raw = item.get('accepted')
7664
  accepted = accepted_raw is True or str(accepted_raw).strip().casefold() == 'true'
7665
+ reviewed_protected = re.sub(r'\s+', ' ', str(item.get('target') or '')).strip()
7666
+ reviewed_target = ''
7667
+ reviewed_defects = ['EMPTY_TARGET']
7668
+ if reviewed_protected:
7669
+ try:
7670
+ reviewed_target = self._restore_canonical_glossary_ir_target(
7671
+ reviewed_protected, ir_by_source[source], block_id=f'canonical-review:{term_id}'
7672
+ )
7673
+ reviewed_defects = self._canonical_target_deterministic_defects(
7674
+ source, reviewed_target, target_lang
7675
+ )
7676
+ except DeepSeekRequestError:
7677
+ reviewed_target = ''
7678
+ reviewed_defects = ['PROTECTED_IR_SKELETON_MISMATCH']
7679
 
7680
+ if accepted and reviewed_protected in {'', protected_candidate}:
 
 
 
7681
  if not defects:
7682
  continue
7683
  raise DeepSeekRequestError(
 
7696
  continue
7697
 
7698
  if not defects:
 
 
 
7699
  reason = 'NON_ACTIONABLE_REJECTION'
7700
+ if accepted and reviewed_protected and reviewed_protected != protected_candidate:
7701
  reason = 'ACCEPTED_BUT_CHANGED'
7702
+ elif reviewed_protected and reviewed_protected != protected_candidate and reviewed_defects:
7703
  reason = 'INVALID_CORRECTION'
7704
  self._record_translation_assurance(
7705
  'CANONICAL_GLOSSARY_REVIEW_INCONCLUSIVE', target_lang=target_lang, severity='diagnostic',