R14-CIT-A1: adaptive citation sufficiency gate

#57
Files changed (2) hide show
  1. core/generation_runner.py +217 -29
  2. core/pipeline_agent.py +3 -3
core/generation_runner.py CHANGED
@@ -34,9 +34,15 @@ from utils.file_parser import extract_text_from_files
34
 
35
  ProgressCallback = Callable[..., None]
36
 
37
- MIN_FINAL_REFERENCES = 50
38
- LITERATURE_EVIDENCE_TARGET = 75
39
- CITATION_PLAN_TARGET = 70
 
 
 
 
 
 
40
  TRANSLATION_CONTRACT_VERSION = 'hierarchical-multilingual-v5'
41
 
42
 
@@ -1493,6 +1499,66 @@ class GenerationRunner:
1493
  "ratio": round(ratio, 4), "status": status, "recommendation": recommendation, "nonfatal": True,
1494
  }
1495
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1496
  def _resolve_paths(self, items) -> list[str]:
1497
  output = []
1498
  for item in items or []:
@@ -1615,16 +1681,16 @@ class GenerationRunner:
1615
  refs_text = str(state.get("refs_text") or "")
1616
  if not refs_text:
1617
  self._progress(8, "S2 + OpenAlex")
 
1618
  refs_text = agent.fetch_references_parallel(
1619
  topic,
1620
  keywords,
1621
- min_required=LITERATURE_EVIDENCE_TARGET,
1622
  progress_callback=lambda: self._progress(9, "S2 + OpenAlex"),
1623
  )
1624
- if len(agent.citation_map) < MIN_FINAL_REFERENCES:
1625
- raise RuntimeError(
1626
- f"Insufficient eligible references: {len(agent.citation_map)}/{MIN_FINAL_REFERENCES}"
1627
- )
1628
  if user_lit_context:
1629
  core_refs = agent.parse_user_core_literature(user_lit_context)
1630
  if core_refs:
@@ -1699,29 +1765,86 @@ class GenerationRunner:
1699
  if jit_texts:
1700
  rag_engine._initialize_chunks([jit_texts])
1701
  self._progress(15, "citation_planning")
1702
- citation_plan = agent.build_claim_evidence_plan(
1703
- adaptive_structure, topic, synthesis_result, mode=mode, target_unique=CITATION_PLAN_TARGET
1704
  )
1705
- canonical_concept_ledger = self._build_canonical_concept_ledger(
1706
- mode, data_metadata, keywords=keywords, adaptive_structure=adaptive_structure,
1707
- research_logic_contract=research_logic_contract,
 
1708
  )
1709
- planned_unique = {
1710
- item.get("tag")
 
 
 
1711
  for items in citation_plan.values()
1712
  for item in items
1713
  if item.get("tag")
1714
  }
1715
- if len(planned_unique) < MIN_FINAL_REFERENCES:
1716
- raise RuntimeError(
1717
- "Insufficient claim-evidence plan before drafting: "
1718
- f"{len(planned_unique)}/{MIN_FINAL_REFERENCES}."
 
 
 
 
 
 
 
 
 
 
 
 
1719
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1720
  state.update(
1721
  {
1722
  "adaptive_structure": adaptive_structure,
1723
  "quote_invocation_plan": quote_invocation_plan,
1724
  "citation_plan": citation_plan,
 
 
 
1725
  "research_logic_contract": research_logic_contract,
1726
  "working_abstract": working_abstract,
1727
  "working_keywords": working_keywords,
@@ -1753,6 +1876,16 @@ class GenerationRunner:
1753
  state.get("research_logic_contract") or agent.global_memory.get("research_logic_contract") or ""
1754
  )
1755
  citation_plan = state.get("citation_plan") if isinstance(state.get("citation_plan"), dict) else {}
 
 
 
 
 
 
 
 
 
 
1756
  translation_sections = dict(state.get('translation_sections') or {})
1757
  translation_glossary = dict(state.get('translation_glossary') or {})
1758
  canonical_concept_ledger = state.get('canonical_concept_ledger')
@@ -1866,9 +1999,20 @@ class GenerationRunner:
1866
  if not citation_plan:
1867
  self._progress(15, "citation_planning")
1868
  citation_plan = agent.build_claim_evidence_plan(
1869
- adaptive_structure, topic, synthesis_result, mode=mode, target_unique=CITATION_PLAN_TARGET
 
1870
  )
 
 
 
 
 
 
 
1871
  state["citation_plan"] = citation_plan
 
 
 
1872
 
1873
  quote_invocation_plan = (
1874
  dict(state.get("quote_invocation_plan") or {}) if mode == "asq" else {}
@@ -2105,9 +2249,13 @@ Output only final academic prose. Do not report the word count."""
2105
  used_after = set(agent.used_citations)
2106
  survived = used_after - used_before
2107
  if assigned_tag_list and not is_quarantined:
 
2108
  required_survival = min(
2109
  len(assigned_tag_list),
2110
- max(1, int(math.ceil(len(assigned_tag_list) * 0.78))),
 
 
 
2111
  )
2112
  if len(survived) < required_survival:
2113
  missing_items = [
@@ -2127,6 +2275,13 @@ Output only final academic prose. Do not report the word count."""
2127
  excluded_tags=used_after | {str(item.get("tag") or "") for item in recovery_items},
2128
  )
2129
  )
 
 
 
 
 
 
 
2130
  supplemental_raw = agent.build_citation_recovery_paragraph(
2131
  recovery_items[:needed], section_name=display_name, section_context=en_resolved
2132
  )
@@ -2140,6 +2295,15 @@ Output only final academic prose. Do not report the word count."""
2140
  en_resolved = en_resolved.rstrip() + "\n\n" + supplemental_resolved.strip()
2141
  used_after = set(agent.used_citations)
2142
  survived = used_after - used_before
 
 
 
 
 
 
 
 
 
2143
  if mode == "asq" and section_id == "findings":
2144
  en_resolved = self._render_planned_qualitative_quotes(
2145
  en_resolved,
@@ -2188,6 +2352,10 @@ Output only final academic prose. Do not report the word count."""
2188
  "context_tail": context_tail,
2189
  "accumulated_deviation": accumulated_deviation,
2190
  "quality_diagnostics": quality_diagnostics,
 
 
 
 
2191
  "agent_state": agent.export_state(),
2192
  "data_metadata": data_metadata,
2193
  }
@@ -2215,18 +2383,33 @@ Output only final academic prose. Do not report the word count."""
2215
  if ref and ref not in seen_refs:
2216
  references.append(f"- {ref}")
2217
  seen_refs.add(ref)
2218
- if len(references) < MIN_FINAL_REFERENCES:
2219
- raise RuntimeError(
2220
- "Insufficient final cited references after semantic validation: "
2221
- f"{len(references)}/{MIN_FINAL_REFERENCES}."
2222
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2223
  full_draft_en = full_draft_en.rstrip() + "\n\n# References\n" + "\n\n".join(references)
2224
  actual_words = self._english_count_for_target(full_draft_en)
2225
  self._upsert_quality_diagnostic(
2226
  quality_diagnostics, self._validate_target_length(full_draft_en, target_words)
2227
  )
2228
  body_only, _reference_tail = agent.validate_reference_tail(
2229
- full_draft_en, minimum_references=MIN_FINAL_REFERENCES
2230
  )
2231
  agent.validate_manuscript_structure(body_only, adaptive_structure, translated=False)
2232
  agent.enforce_resolved_claim_specific_citations(body_only)
@@ -2245,7 +2428,7 @@ Output only final academic prose. Do not report the word count."""
2245
  full_draft_en = replace_abstract(full_draft_en, final_abstract)
2246
  full_draft_en = self._replace_keywords(full_draft_en, final_keywords)
2247
  body_only, _reference_tail = agent.validate_reference_tail(
2248
- full_draft_en, minimum_references=MIN_FINAL_REFERENCES
2249
  )
2250
  agent.validate_manuscript_structure(body_only, adaptive_structure, translated=False)
2251
  agent.enforce_resolved_claim_specific_citations(body_only)
@@ -2282,6 +2465,11 @@ Output only final academic prose. Do not report the word count."""
2282
  "final_abstract": final_abstract,
2283
  "final_keywords": final_keywords,
2284
  "front_matter_phase": "final",
 
 
 
 
 
2285
  "quality_diagnostics": quality_diagnostics,
2286
  "english_finalized": True,
2287
  "translation_complete": False,
@@ -2305,7 +2493,7 @@ Output only final academic prose. Do not report the word count."""
2305
  )
2306
  self._shutdown_translation_worker(wait=True)
2307
  body_only, reference_tail = agent.validate_reference_tail(
2308
- full_draft_en, minimum_references=MIN_FINAL_REFERENCES
2309
  )
2310
  section_order = self._unique_top_level_sections(adaptive_structure)
2311
  if not section_order:
 
34
 
35
  ProgressCallback = Callable[..., None]
36
 
37
+ CITATION_SEMANTIC_SURVIVAL_RATIO = 0.78
38
+ CITATION_ROLE_RULES = {
39
+ "introduction": (3, 180),
40
+ "theory": (3, 180),
41
+ "theoretical_framing": (3, 180),
42
+ "methodology": (2, 280),
43
+ "methods": (2, 280),
44
+ "discussion": (2, 220),
45
+ }
46
  TRANSLATION_CONTRACT_VERSION = 'hierarchical-multilingual-v5'
47
 
48
 
 
1499
  "ratio": round(ratio, 4), "status": status, "recommendation": recommendation, "nonfatal": True,
1500
  }
1501
 
1502
+ @staticmethod
1503
+ def _citation_eligible_section_ids(mode: str) -> set[str]:
1504
+ return (
1505
+ {"introduction", "theoretical_framing", "methods", "discussion"}
1506
+ if str(mode).lower() == "asq"
1507
+ else {"introduction", "theory", "methodology", "discussion"}
1508
+ )
1509
+
1510
+ @classmethod
1511
+ def _adaptive_citation_requirements(cls, tasks, mode: str):
1512
+ """Derive evidence demand and semantic-survival reserve from manuscript units."""
1513
+ allowed = cls._citation_eligible_section_ids(mode)
1514
+ minimums = {}
1515
+ reserve_targets = {}
1516
+ for task in tasks or []:
1517
+ section_id = str(task.get("section_id") or "")
1518
+ display_name = str(task.get("display_name") or "").strip()
1519
+ if section_id not in allowed or not display_name:
1520
+ continue
1521
+ base, words_per_source = CITATION_ROLE_RULES.get(section_id, (2, 240))
1522
+ target_words = max(1, int(task.get("target_words") or 1))
1523
+ minimum = max(base, int(math.ceil(target_words / max(1, words_per_source))))
1524
+ reserve = max(
1525
+ minimum,
1526
+ int(math.ceil(minimum / max(0.01, CITATION_SEMANTIC_SURVIVAL_RATIO))),
1527
+ )
1528
+ minimums[display_name] = minimum
1529
+ reserve_targets[display_name] = reserve
1530
+ return minimums, reserve_targets
1531
+
1532
+ @staticmethod
1533
+ def _adaptive_literature_candidate_target(target_words: int, agent) -> int:
1534
+ """Adaptive retrieval aspiration; never a final reference-count quota."""
1535
+ dynamic = max(24, int(math.ceil(max(1, int(target_words or 1)) / 90.0)))
1536
+ configured_cap = max(1, int(getattr(agent, "lit_target_papers", dynamic) or dynamic))
1537
+ return min(configured_cap, dynamic)
1538
+
1539
+ @staticmethod
1540
+ def _citation_task_context(task, research_logic_contract="", synthesis_result="") -> str:
1541
+ parts = [
1542
+ str(task.get("display_name") or ""),
1543
+ str(task.get("required_content") or ""),
1544
+ " ".join(str(value) for value in (task.get("concepts") or []) if str(value).strip()),
1545
+ str(research_logic_contract or "")[:1600],
1546
+ str(synthesis_result or "")[:1600],
1547
+ ]
1548
+ return re.sub(r"\s+", " ", " ".join(part for part in parts if part)).strip()
1549
+
1550
+ @staticmethod
1551
+ def _literature_insufficient_error(shortfalls) -> RuntimeError:
1552
+ details = "; ".join(
1553
+ f"{name}={actual}/{required}"
1554
+ for name, actual, required in (shortfalls or [])[:8]
1555
+ )
1556
+ return RuntimeError(
1557
+ "E_LITERATURE_INSUFFICIENT: high-relevance claim-specific literature is insufficient "
1558
+ f"for the planned manuscript units ({details}). Please revise the research topic or "
1559
+ "keywords and try again; unrelated references will not be used to satisfy a quota."
1560
+ )
1561
+
1562
  def _resolve_paths(self, items) -> list[str]:
1563
  output = []
1564
  for item in items or []:
 
1681
  refs_text = str(state.get("refs_text") or "")
1682
  if not refs_text:
1683
  self._progress(8, "S2 + OpenAlex")
1684
+ literature_candidate_target = self._adaptive_literature_candidate_target(target_words, agent)
1685
  refs_text = agent.fetch_references_parallel(
1686
  topic,
1687
  keywords,
1688
+ min_required=literature_candidate_target,
1689
  progress_callback=lambda: self._progress(9, "S2 + OpenAlex"),
1690
  )
1691
+ if not agent.citation_map:
1692
+ raise self._literature_insufficient_error([("retrieved literature pool", 0, 1)])
1693
+ state["literature_candidate_target"] = literature_candidate_target
 
1694
  if user_lit_context:
1695
  core_refs = agent.parse_user_core_literature(user_lit_context)
1696
  if core_refs:
 
1765
  if jit_texts:
1766
  rag_engine._initialize_chunks([jit_texts])
1767
  self._progress(15, "citation_planning")
1768
+ citation_minimums, citation_reserve_targets = self._adaptive_citation_requirements(
1769
+ adaptive_structure, mode
1770
  )
1771
+ citation_plan_target = sum(citation_reserve_targets.values())
1772
+ citation_plan = agent.build_claim_evidence_plan(
1773
+ adaptive_structure, topic, synthesis_result, mode=mode,
1774
+ target_unique=max(1, citation_plan_target),
1775
  )
1776
+
1777
+ # Fill unit-specific reserve gaps before expensive drafting. Existing
1778
+ # retrieved evidence is preferred; bounded contextual JIT is last resort.
1779
+ planned_tags = {
1780
+ str(item.get("tag") or "")
1781
  for items in citation_plan.values()
1782
  for item in items
1783
  if item.get("tag")
1784
  }
1785
+ task_by_name = {
1786
+ str(task.get("display_name") or ""): task
1787
+ for task in adaptive_structure
1788
+ if str(task.get("display_name") or "")
1789
+ }
1790
+ for display_name, reserve_target in citation_reserve_targets.items():
1791
+ planned_items = citation_plan.setdefault(display_name, [])
1792
+ shortfall = max(0, reserve_target - len(planned_items))
1793
+ if shortfall <= 0:
1794
+ continue
1795
+ section_context = self._citation_task_context(
1796
+ task_by_name.get(display_name, {}), research_logic_contract, synthesis_result
1797
+ )
1798
+ recovery_items = agent.select_contextual_citation_recovery_items(
1799
+ [], section_name=display_name, section_context=section_context,
1800
+ needed=shortfall, excluded_tags=planned_tags,
1801
  )
1802
+ for item in recovery_items:
1803
+ tag = str(item.get("tag") or "")
1804
+ if tag and tag not in planned_tags:
1805
+ planned_items.append(item)
1806
+ planned_tags.add(tag)
1807
+ # External JIT is not used merely to hit the reserve target.
1808
+ # It is triggered only when the unit remains below its actual
1809
+ # evidence-demand minimum after exhausting already retrieved
1810
+ # relevant literature.
1811
+ minimum_shortfall = max(
1812
+ 0, int(citation_minimums.get(display_name, 0)) - len(planned_items)
1813
+ )
1814
+ if minimum_shortfall > 0:
1815
+ jit_items = agent.fetch_contextual_jit_recovery_items(
1816
+ topic=topic, section_name=display_name, section_context=section_context,
1817
+ needed=minimum_shortfall, excluded_tags=planned_tags,
1818
+ )
1819
+ for item in jit_items:
1820
+ tag = str(item.get("tag") or "")
1821
+ if tag and tag not in planned_tags:
1822
+ planned_items.append(item)
1823
+ planned_tags.add(tag)
1824
+
1825
+ shortfalls = [
1826
+ (name, len(citation_plan.get(name) or []), required)
1827
+ for name, required in citation_minimums.items()
1828
+ if len(citation_plan.get(name) or []) < required
1829
+ ]
1830
+ if shortfalls:
1831
+ raise self._literature_insufficient_error(shortfalls)
1832
+ logging.info(
1833
+ "Adaptive citation feasibility passed: minimum_total=%s reserve_target=%s planned_unique=%s units=%s",
1834
+ sum(citation_minimums.values()), citation_plan_target, len(planned_tags), len(citation_minimums),
1835
+ )
1836
+ canonical_concept_ledger = self._build_canonical_concept_ledger(
1837
+ mode, data_metadata, keywords=keywords, adaptive_structure=adaptive_structure,
1838
+ research_logic_contract=research_logic_contract,
1839
+ )
1840
  state.update(
1841
  {
1842
  "adaptive_structure": adaptive_structure,
1843
  "quote_invocation_plan": quote_invocation_plan,
1844
  "citation_plan": citation_plan,
1845
+ "citation_minimums": citation_minimums,
1846
+ "citation_reserve_targets": citation_reserve_targets,
1847
+ "citation_validated_tags_by_unit": {},
1848
  "research_logic_contract": research_logic_contract,
1849
  "working_abstract": working_abstract,
1850
  "working_keywords": working_keywords,
 
1876
  state.get("research_logic_contract") or agent.global_memory.get("research_logic_contract") or ""
1877
  )
1878
  citation_plan = state.get("citation_plan") if isinstance(state.get("citation_plan"), dict) else {}
1879
+ citation_minimums = dict(state.get("citation_minimums") or {})
1880
+ citation_reserve_targets = dict(state.get("citation_reserve_targets") or {})
1881
+ citation_validated_tags_by_unit = {
1882
+ str(name): list(tags or [])
1883
+ for name, tags in dict(state.get("citation_validated_tags_by_unit") or {}).items()
1884
+ }
1885
+ if not citation_minimums:
1886
+ citation_minimums, citation_reserve_targets = self._adaptive_citation_requirements(
1887
+ adaptive_structure, mode
1888
+ )
1889
  translation_sections = dict(state.get('translation_sections') or {})
1890
  translation_glossary = dict(state.get('translation_glossary') or {})
1891
  canonical_concept_ledger = state.get('canonical_concept_ledger')
 
1999
  if not citation_plan:
2000
  self._progress(15, "citation_planning")
2001
  citation_plan = agent.build_claim_evidence_plan(
2002
+ adaptive_structure, topic, synthesis_result, mode=mode,
2003
+ target_unique=max(1, sum(citation_reserve_targets.values())),
2004
  )
2005
+ shortfalls = [
2006
+ (name, len(citation_plan.get(name) or []), required)
2007
+ for name, required in citation_minimums.items()
2008
+ if len(citation_plan.get(name) or []) < required
2009
+ ]
2010
+ if shortfalls:
2011
+ raise self._literature_insufficient_error(shortfalls)
2012
  state["citation_plan"] = citation_plan
2013
+ state["citation_minimums"] = citation_minimums
2014
+ state["citation_reserve_targets"] = citation_reserve_targets
2015
+ state["citation_validated_tags_by_unit"] = citation_validated_tags_by_unit
2016
 
2017
  quote_invocation_plan = (
2018
  dict(state.get("quote_invocation_plan") or {}) if mode == "asq" else {}
 
2249
  used_after = set(agent.used_citations)
2250
  survived = used_after - used_before
2251
  if assigned_tag_list and not is_quarantined:
2252
+ adaptive_minimum = max(1, int(citation_minimums.get(display_name, 1)))
2253
  required_survival = min(
2254
  len(assigned_tag_list),
2255
+ max(
2256
+ adaptive_minimum,
2257
+ int(math.ceil(len(assigned_tag_list) * CITATION_SEMANTIC_SURVIVAL_RATIO)),
2258
+ ),
2259
  )
2260
  if len(survived) < required_survival:
2261
  missing_items = [
 
2275
  excluded_tags=used_after | {str(item.get("tag") or "") for item in recovery_items},
2276
  )
2277
  )
2278
+ planned_tags_for_unit = {str(item.get("tag") or "") for item in planned_items}
2279
+ for recovery_item in recovery_items[:needed]:
2280
+ tag = str(recovery_item.get("tag") or "")
2281
+ if tag and tag not in planned_tags_for_unit:
2282
+ planned_items.append(recovery_item)
2283
+ planned_tags_for_unit.add(tag)
2284
+ citation_plan[display_name] = planned_items
2285
  supplemental_raw = agent.build_citation_recovery_paragraph(
2286
  recovery_items[:needed], section_name=display_name, section_context=en_resolved
2287
  )
 
2295
  en_resolved = en_resolved.rstrip() + "\n\n" + supplemental_resolved.strip()
2296
  used_after = set(agent.used_citations)
2297
  survived = used_after - used_before
2298
+ if assigned_tag_list and not is_quarantined:
2299
+ adaptive_minimum = max(1, int(citation_minimums.get(display_name, 1)))
2300
+ if len(survived) < adaptive_minimum:
2301
+ raise self._literature_insufficient_error(
2302
+ [(display_name, len(survived), adaptive_minimum)]
2303
+ )
2304
+ citation_validated_tags_by_unit[display_name] = sorted(
2305
+ survived, key=agent._citation_tag_number
2306
+ )
2307
  if mode == "asq" and section_id == "findings":
2308
  en_resolved = self._render_planned_qualitative_quotes(
2309
  en_resolved,
 
2352
  "context_tail": context_tail,
2353
  "accumulated_deviation": accumulated_deviation,
2354
  "quality_diagnostics": quality_diagnostics,
2355
+ "citation_plan": citation_plan,
2356
+ "citation_minimums": citation_minimums,
2357
+ "citation_reserve_targets": citation_reserve_targets,
2358
+ "citation_validated_tags_by_unit": citation_validated_tags_by_unit,
2359
  "agent_state": agent.export_state(),
2360
  "data_metadata": data_metadata,
2361
  }
 
2383
  if ref and ref not in seen_refs:
2384
  references.append(f"- {ref}")
2385
  seen_refs.add(ref)
2386
+ if not references:
2387
+ raise self._literature_insufficient_error([("final validated references", 0, 1)])
2388
+ final_shortfalls = []
2389
+ for name, minimum in citation_minimums.items():
2390
+ validated_tags = set(citation_validated_tags_by_unit.get(name) or [])
2391
+ if not validated_tags:
2392
+ validated_tags = {
2393
+ str(item.get("tag") or "")
2394
+ for item in (citation_plan.get(name) or [])
2395
+ if str(item.get("tag") or "") in agent.used_citations
2396
+ }
2397
+ if len(validated_tags) < int(minimum):
2398
+ final_shortfalls.append((name, len(validated_tags), int(minimum)))
2399
+ if final_shortfalls:
2400
+ raise self._literature_insufficient_error(final_shortfalls)
2401
+ final_reference_count = len(references)
2402
+ logging.info(
2403
+ "Adaptive citation final Gate passed: validated_references=%s units=%s",
2404
+ final_reference_count, len(citation_minimums),
2405
+ )
2406
  full_draft_en = full_draft_en.rstrip() + "\n\n# References\n" + "\n\n".join(references)
2407
  actual_words = self._english_count_for_target(full_draft_en)
2408
  self._upsert_quality_diagnostic(
2409
  quality_diagnostics, self._validate_target_length(full_draft_en, target_words)
2410
  )
2411
  body_only, _reference_tail = agent.validate_reference_tail(
2412
+ full_draft_en, minimum_references=max(1, final_reference_count)
2413
  )
2414
  agent.validate_manuscript_structure(body_only, adaptive_structure, translated=False)
2415
  agent.enforce_resolved_claim_specific_citations(body_only)
 
2428
  full_draft_en = replace_abstract(full_draft_en, final_abstract)
2429
  full_draft_en = self._replace_keywords(full_draft_en, final_keywords)
2430
  body_only, _reference_tail = agent.validate_reference_tail(
2431
+ full_draft_en, minimum_references=max(1, final_reference_count)
2432
  )
2433
  agent.validate_manuscript_structure(body_only, adaptive_structure, translated=False)
2434
  agent.enforce_resolved_claim_specific_citations(body_only)
 
2465
  "final_abstract": final_abstract,
2466
  "final_keywords": final_keywords,
2467
  "front_matter_phase": "final",
2468
+ "final_reference_count": final_reference_count,
2469
+ "citation_plan": citation_plan,
2470
+ "citation_minimums": citation_minimums,
2471
+ "citation_reserve_targets": citation_reserve_targets,
2472
+ "citation_validated_tags_by_unit": citation_validated_tags_by_unit,
2473
  "quality_diagnostics": quality_diagnostics,
2474
  "english_finalized": True,
2475
  "translation_complete": False,
 
2493
  )
2494
  self._shutdown_translation_worker(wait=True)
2495
  body_only, reference_tail = agent.validate_reference_tail(
2496
+ full_draft_en, minimum_references=max(1, int(state.get("final_reference_count") or 1))
2497
  )
2498
  section_order = self._unique_top_level_sections(adaptive_structure)
2499
  if not section_order:
core/pipeline_agent.py CHANGED
@@ -2698,9 +2698,9 @@ INPUT:
2698
 
2699
  At most one query is issued to each literature backend. New references
2700
  are admitted only when title/abstract evidence has distinctive lexical
2701
- overlap with the current manuscript unit; otherwise the final >=50 gate
2702
- is allowed to fail rather than padding the manuscript with irrelevant
2703
- literature.
2704
  """
2705
  needed = max(0, int(needed or 0))
2706
  if needed <= 0:
 
2698
 
2699
  At most one query is issued to each literature backend. New references
2700
  are admitted only when title/abstract evidence has distinctive lexical
2701
+ overlap with the current manuscript unit; otherwise the adaptive
2702
+ evidence-sufficiency Gate is allowed to fail rather than padding the
2703
+ manuscript with irrelevant literature.
2704
  """
2705
  needed = max(0, int(needed or 0))
2706
  if needed <= 0: