A5-R3-R7-R2C-R3 adaptive finalization recovery scoped deployment

#119
Files changed (2) hide show
  1. core/generation_runner.py +39 -2
  2. core/pipeline_agent.py +55 -10
core/generation_runner.py CHANGED
@@ -5229,7 +5229,25 @@ The previous attempt for this same manuscript unit was rejected by validation. R
5229
  agent.enforce_resolved_claim_specific_citations(body_only)
5230
  post_front_ledger = agent.finalize_resolved_citation_ledger(body_only)
5231
  if set(post_front_ledger) != set(final_citation_ledger):
5232
- raise RuntimeError("final_front_matter_changed_citation_ledger")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5233
  agent.validate_final_citation_bijection(body_only, _reference_tail, final_citation_ledger)
5234
  self._upsert_quality_diagnostic(
5235
  quality_diagnostics, self._validate_target_length(full_draft_en, target_words)
@@ -5451,12 +5469,31 @@ The previous attempt for this same manuscript unit was rejected by validation. R
5451
  )
5452
  try:
5453
  agent.validate_translation_alignment(full_draft_en, full_draft_translated)
5454
- agent.validate_reference_preservation(full_draft_en, full_draft_translated, target_lang)
5455
  except Exception as exc:
5456
  self._record_adaptive_completion_residual(
5457
  quality_notes, [{'type':'translation_final_alignment_review','reason':str(exc)[:500]}],
5458
  section_name='Target-language manuscript', stage='translation_final_review'
5459
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5460
  self._publish_target_preview(
5461
  full_draft_translated, 'translation_complete', force=True, complete=True
5462
  )
 
5229
  agent.enforce_resolved_claim_specific_citations(body_only)
5230
  post_front_ledger = agent.finalize_resolved_citation_ledger(body_only)
5231
  if set(post_front_ledger) != set(final_citation_ledger):
5232
+ logging.warning(
5233
+ "Adaptive finalization resealed citation ledger after Final Front Matter | before=%s after=%s",
5234
+ len(final_citation_ledger), len(post_front_ledger),
5235
+ )
5236
+ # Final Front Matter is allowed to add/remove an already-authoritative
5237
+ # citation. Rebuild References deterministically from the post-front
5238
+ # resolved body instead of discarding an otherwise complete manuscript.
5239
+ # Orphan/ambiguous citations still fail closed inside ledger finalization.
5240
+ final_citation_ledger = list(post_front_ledger)
5241
+ references = agent.build_final_reference_lines(final_citation_ledger)
5242
+ final_reference_count = len(references)
5243
+ full_draft_en = body_only.rstrip() + "\n\n# References\n" + "\n\n".join(references)
5244
+ coverage_summary = dict(state.get("evidence_coverage_summary") or {})
5245
+ if coverage_summary:
5246
+ coverage_summary["final_reference_count"] = final_reference_count
5247
+ state["evidence_coverage_summary"] = coverage_summary
5248
+ body_only, _reference_tail = agent.validate_reference_tail(
5249
+ full_draft_en, minimum_references=final_reference_count
5250
+ )
5251
  agent.validate_final_citation_bijection(body_only, _reference_tail, final_citation_ledger)
5252
  self._upsert_quality_diagnostic(
5253
  quality_diagnostics, self._validate_target_length(full_draft_en, target_words)
 
5469
  )
5470
  try:
5471
  agent.validate_translation_alignment(full_draft_en, full_draft_translated)
 
5472
  except Exception as exc:
5473
  self._record_adaptive_completion_residual(
5474
  quality_notes, [{'type':'translation_final_alignment_review','reason':str(exc)[:500]}],
5475
  section_name='Target-language manuscript', stage='translation_final_review'
5476
  )
5477
+ try:
5478
+ agent.validate_reference_preservation(full_draft_en, full_draft_translated, target_lang)
5479
+ except Exception as exc:
5480
+ logging.warning(
5481
+ "Adaptive target References rebind | language=%s reason=%s",
5482
+ target_lang, str(exc)[:300],
5483
+ )
5484
+ full_draft_translated = agent.rebind_target_reference_tail(
5485
+ full_draft_translated, reference_tail, target_lang
5486
+ )
5487
+ # References are identity-bearing academic data. Recovery is
5488
+ # deterministic and provider-free; if this still fails, package
5489
+ # generation must stop rather than ship a corrupted bibliography.
5490
+ agent.validate_reference_preservation(
5491
+ full_draft_en, full_draft_translated, target_lang
5492
+ )
5493
+ self._record_adaptive_completion_residual(
5494
+ quality_notes, [{'type':'target_reference_tail_rebound','reason':str(exc)[:500]}],
5495
+ section_name='Target-language manuscript', stage='translation_final_recovery'
5496
+ )
5497
  self._publish_target_preview(
5498
  full_draft_translated, 'translation_complete', force=True, complete=True
5499
  )
core/pipeline_agent.py CHANGED
@@ -2659,15 +2659,52 @@ If the immutable hypothesis contract is non-empty, preserve its H-identifiers an
2659
  )
2660
  return sorted(ledger, key=self._citation_tag_number)
2661
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2662
  def build_final_reference_lines(self, ledger_tags):
2663
- """Build one English reference entry per authoritative ledger source."""
2664
  references = {}
2665
  for tag in ledger_tags or []:
2666
  metadata = self.citation_map.get(str(tag)) or {}
2667
- ref = str(metadata.get('ref') or '').strip()
2668
  if not ref:
2669
  raise DeepSeekRequestError(f'Final citation ledger metadata missing reference text: {tag}')
2670
- references.setdefault(re.sub(r'\s+', ' ', ref).casefold(), ref)
2671
  # Reader-facing References follow the conventional alphabetical order.
2672
  return [f"- {ref}" for ref in sorted(references.values(), key=lambda value: value.casefold())]
2673
 
@@ -2680,18 +2717,14 @@ If the immutable hypothesis contract is non-empty, preserve its H-identifiers an
2680
  for tag in ledger:
2681
  metadata = self.citation_map.get(str(tag)) or {}
2682
  marker = str(metadata.get('in_text') or '').strip()
2683
- ref = str(metadata.get('ref') or '').strip()
2684
  if not marker or not ref:
2685
  raise DeepSeekRequestError(f'Final citation ledger is incomplete: {tag}')
2686
  if marker not in body:
2687
  raise DeepSeekRequestError(f'Final citation ledger contains a citation absent from body: {tag}')
2688
- expected.append(re.sub(r'\s+', ' ', ref).strip())
2689
 
2690
- actual = [
2691
- re.sub(r'\s+', ' ', line[2:]).strip()
2692
- for line in tail.splitlines()
2693
- if line.strip().startswith('- ')
2694
- ]
2695
  if len(actual) != len(set(actual)):
2696
  raise DeepSeekRequestError('Final References contains duplicate entries.')
2697
  if set(actual) != set(expected):
@@ -2708,6 +2741,18 @@ If the immutable hypothesis contract is non-empty, preserve its H-identifiers an
2708
  )
2709
  return True
2710
 
 
 
 
 
 
 
 
 
 
 
 
 
2711
  @classmethod
2712
  def validate_reference_preservation(cls, english_text, translated_text, target_lang):
2713
  """References are immutable English data in ZH/RU/KK deliverables."""
 
2659
  )
2660
  return sorted(ledger, key=self._citation_tag_number)
2661
 
2662
+ @staticmethod
2663
+ def _canonical_reference_text(value):
2664
+ """Canonicalize formatting-only reference variance without changing identity.
2665
+
2666
+ Provider metadata can legally contain embedded CR/LF, tabs, Unicode line
2667
+ separators, non-breaking spaces, or zero-width formatting characters. A
2668
+ reference is one bibliographic object, so those transport/layout artifacts
2669
+ must never turn one source into multiple Markdown lines at finalization.
2670
+ """
2671
+ text = str(value or '')
2672
+ text = text.replace('\r\n', '\n').replace('\r', '\n')
2673
+ text = re.sub(r'[\u200b\u2060\ufeff]+', '', text)
2674
+ return re.sub(r'\s+', ' ', text).strip()
2675
+
2676
+ @classmethod
2677
+ def _canonical_reference_entries_from_tail(cls, reference_tail):
2678
+ """Parse bullet references while deterministically joining wrapped lines."""
2679
+ entries = []
2680
+ current = []
2681
+ for raw_line in str(reference_tail or '').replace('\r\n', '\n').replace('\r', '\n').split('\n'):
2682
+ line = raw_line.strip()
2683
+ if not line:
2684
+ continue
2685
+ if line.startswith('- '):
2686
+ if current:
2687
+ entries.append(cls._canonical_reference_text(' '.join(current)))
2688
+ current = [line[2:].strip()]
2689
+ elif current:
2690
+ current.append(line)
2691
+ else:
2692
+ raise DeepSeekRequestError(
2693
+ 'Final References contains non-entry text before the first reference bullet.'
2694
+ )
2695
+ if current:
2696
+ entries.append(cls._canonical_reference_text(' '.join(current)))
2697
+ return entries
2698
+
2699
  def build_final_reference_lines(self, ledger_tags):
2700
+ """Build one canonical single-line English reference per ledger source."""
2701
  references = {}
2702
  for tag in ledger_tags or []:
2703
  metadata = self.citation_map.get(str(tag)) or {}
2704
+ ref = self._canonical_reference_text(metadata.get('ref') or '')
2705
  if not ref:
2706
  raise DeepSeekRequestError(f'Final citation ledger metadata missing reference text: {tag}')
2707
+ references.setdefault(ref.casefold(), ref)
2708
  # Reader-facing References follow the conventional alphabetical order.
2709
  return [f"- {ref}" for ref in sorted(references.values(), key=lambda value: value.casefold())]
2710
 
 
2717
  for tag in ledger:
2718
  metadata = self.citation_map.get(str(tag)) or {}
2719
  marker = str(metadata.get('in_text') or '').strip()
2720
+ ref = self._canonical_reference_text(metadata.get('ref') or '')
2721
  if not marker or not ref:
2722
  raise DeepSeekRequestError(f'Final citation ledger is incomplete: {tag}')
2723
  if marker not in body:
2724
  raise DeepSeekRequestError(f'Final citation ledger contains a citation absent from body: {tag}')
2725
+ expected.append(ref)
2726
 
2727
+ actual = self._canonical_reference_entries_from_tail(tail)
 
 
 
 
2728
  if len(actual) != len(set(actual)):
2729
  raise DeepSeekRequestError('Final References contains duplicate entries.')
2730
  if set(actual) != set(expected):
 
2741
  )
2742
  return True
2743
 
2744
+ @classmethod
2745
+ def rebind_target_reference_tail(cls, translated_text, frozen_reference_tail, target_lang):
2746
+ """Replace a target References tail from frozen English bytes, without LLM use."""
2747
+ target = str(translated_text or '').strip()
2748
+ heading = cls._localized_reference_heading(target_lang)
2749
+ matches = list(re.finditer(rf'(?m)^#\s+{re.escape(heading)}\s*$', target))
2750
+ body = target[:matches[0].start()].rstrip() if matches else target.rstrip()
2751
+ tail = str(frozen_reference_tail or '').strip()
2752
+ if not tail:
2753
+ return body + f"\n\n# {heading}\n"
2754
+ return body + f"\n\n# {heading}\n" + tail
2755
+
2756
  @classmethod
2757
  def validate_reference_preservation(cls, english_text, translated_text, target_lang):
2758
  """References are immutable English data in ZH/RU/KK deliverables."""