R14-TQ2-PQ1-R3-R1 Evidence-Limited Completion + Four-Language Detailed Progress

#64
app.py CHANGED
@@ -688,10 +688,58 @@ os.makedirs(_user_workspace, exist_ok=True)
688
  ACTIVE_TASK_STATUSES = {"created", "running", "resumable", "cancelling"}
689
  LIVE_TASK_STATUSES = {"created", "running", "cancelling"}
690
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
691
  def _render_monitor_box(task: dict, lang_code: str) -> None:
692
  """Render the approved monitor location with DOM targets for live updates."""
693
  status = str(task.get("status") or "")
694
  node = str(task.get("current_node") or status or "")
 
 
695
  progress = max(0, min(100, int(task.get("progress_pct") or 0)))
696
  checkpoint_seq = int(task.get("checkpoint_seq") or 0)
697
  checkpoint_stage = str(task.get("checkpoint_stage") or "")
@@ -703,7 +751,7 @@ def _render_monitor_box(task: dict, lang_code: str) -> None:
703
  if status in ACTIVE_TASK_STATUSES:
704
  progress_markup = f"""
705
  <div id='sp-live-progress-wrap' style='margin:0 0 12px 0;'>
706
- <div id='sp-live-progress-text' style='margin-bottom:7px;'>{progress}% — {node}</div>
707
  <div style='width:100%;height:10px;background:rgba(128,128,128,.24);border-radius:999px;overflow:hidden;'>
708
  <div id='sp-live-progress-fill' style='width:{progress}%;height:100%;background:currentColor;border-radius:999px;transition:width .35s ease;'></div>
709
  </div>
@@ -713,11 +761,12 @@ def _render_monitor_box(task: dict, lang_code: str) -> None:
713
  st.markdown(f"""
714
  {progress_markup}
715
  <div class='log-box' id='sp-live-monitor'>
716
- <div><b>{tr(lang_code, 'monitor_pipeline')}:</b> <span id='sp-live-status'>{status}</span></div>
717
- <div><b>{tr(lang_code, 'monitor_node')}:</b> <span id='sp-live-node'>{node}</span></div>
718
- <div><b>{tr(lang_code, 'monitor_log')}:</b> checkpoint <span id='sp-live-checkpoint-seq'>{checkpoint_seq}</span> · <span id='sp-live-checkpoint-stage'>{checkpoint_stage}</span></div>
 
719
  <div id='sp-live-activity-feed' style='margin:8px 0 10px 0;max-height:150px;overflow:auto;line-height:1.55;'>
720
- <div>• {node}</div>
721
  </div><hr>
722
  <div><b>{tr(lang_code, 'monitor_compute')}:</b></div>
723
  <div style='display:flex;gap:8px;'>
@@ -767,6 +816,27 @@ def _watch_task_status_live(task: dict) -> dict | None:
767
  "outline_planning": tr(str(st.session_state.get("sp_lang_code") or "zh"), "draft_stage_outline"),
768
  "citation_planning": tr(str(st.session_state.get("sp_lang_code") or "zh"), "draft_stage_citation"),
769
  "writing": tr(str(st.session_state.get("sp_lang_code") or "zh"), "draft_stage_writing"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
770
  "progress": tr(str(st.session_state.get("sp_lang_code") or "zh"), "draft_detail_progress"),
771
  "s2": tr(str(st.session_state.get("sp_lang_code") or "zh"), "draft_detail_s2"),
772
  "oa": tr(str(st.session_state.get("sp_lang_code") or "zh"), "draft_detail_oa"),
@@ -998,6 +1068,8 @@ def _render_task_status_once(lang_code: str) -> None:
998
  f"<div id='sp-completed-download-anchor' data-sp-task-id='{html.escape(task_id, quote=True)}'></div>",
999
  unsafe_allow_html=True,
1000
  )
 
 
1001
  st.success(tr(lang_code, "success"))
1002
  st.download_button(label=tr(lang_code, "download"), data=output_path.read_bytes(), file_name=output_path.name, mime="application/zip")
1003
  _scroll_to_completed_download_once(task_id)
 
688
  ACTIVE_TASK_STATUSES = {"created", "running", "resumable", "cancelling"}
689
  LIVE_TASK_STATUSES = {"created", "running", "cancelling"}
690
 
691
+ def _localized_task_status(lang_code: str, status: str) -> str:
692
+ key = f"task_status_{str(status or '').strip().lower()}"
693
+ value = tr(lang_code, key)
694
+ return value if value != key else tr(lang_code, "draft_stage_processing")
695
+
696
+ def _localized_monitor_node(lang_code: str, node: str) -> str:
697
+ """Translate stable executor progress codes without exposing engineering identifiers."""
698
+ value = str(node or "").strip()
699
+ exact = {
700
+ "inputs_and_evidence": "draft_stage_inputs",
701
+ "S2 + OpenAlex": "draft_stage_search",
702
+ "literature_synthesis": "draft_stage_synthesis",
703
+ "research_logic": "draft_stage_research_logic",
704
+ "working_front_matter": "draft_stage_working_front",
705
+ "outline_planning": "draft_stage_outline",
706
+ "citation_planning": "draft_stage_citation",
707
+ "english_validation": "draft_stage_english_validation",
708
+ "final_front_matter": "draft_stage_final_front",
709
+ "translation": "draft_stage_translation",
710
+ "translation_complete": "translation_complete_label",
711
+ "packaging": "draft_stage_packaging",
712
+ "package_validation": "draft_stage_package_validation",
713
+ "package_validated": "draft_stage_package_validated",
714
+ }
715
+ if value in exact:
716
+ return tr(lang_code, exact[value])
717
+ for prefix, key in (
718
+ ("writing:", "draft_stage_writing_unit"),
719
+ ("citation_audit:", "draft_stage_citation_audit_unit"),
720
+ ("citation_recovery:", "draft_stage_citation_recovery_unit"),
721
+ ("writing_complete:", "draft_stage_writing_complete_unit"),
722
+ ("translation_final_gate:", "draft_stage_translation"),
723
+ ):
724
+ if value.startswith(prefix):
725
+ parts = value[len(prefix):].split(":")
726
+ if key == "draft_stage_translation":
727
+ return tr(lang_code, key)
728
+ if len(parts) >= 2:
729
+ return tr(lang_code, key, current=parts[0], total=parts[1])
730
+ return tr(lang_code, "draft_stage_processing")
731
+ if value.startswith("translation_"):
732
+ return tr(lang_code, "draft_stage_translation")
733
+ if value.startswith("section_"):
734
+ return tr(lang_code, "draft_stage_writing")
735
+ return tr(lang_code, "draft_stage_processing")
736
+
737
  def _render_monitor_box(task: dict, lang_code: str) -> None:
738
  """Render the approved monitor location with DOM targets for live updates."""
739
  status = str(task.get("status") or "")
740
  node = str(task.get("current_node") or status or "")
741
+ status_label = _localized_task_status(lang_code, status)
742
+ node_label = _localized_monitor_node(lang_code, node)
743
  progress = max(0, min(100, int(task.get("progress_pct") or 0)))
744
  checkpoint_seq = int(task.get("checkpoint_seq") or 0)
745
  checkpoint_stage = str(task.get("checkpoint_stage") or "")
 
751
  if status in ACTIVE_TASK_STATUSES:
752
  progress_markup = f"""
753
  <div id='sp-live-progress-wrap' style='margin:0 0 12px 0;'>
754
+ <div id='sp-live-progress-text' style='margin-bottom:7px;'>{progress}% — {html.escape(node_label)}</div>
755
  <div style='width:100%;height:10px;background:rgba(128,128,128,.24);border-radius:999px;overflow:hidden;'>
756
  <div id='sp-live-progress-fill' style='width:{progress}%;height:100%;background:currentColor;border-radius:999px;transition:width .35s ease;'></div>
757
  </div>
 
761
  st.markdown(f"""
762
  {progress_markup}
763
  <div class='log-box' id='sp-live-monitor'>
764
+ <div><b>{tr(lang_code, 'monitor_pipeline')}:</b> <span id='sp-live-status'>{html.escape(status_label)}</span></div>
765
+ <div><b>{tr(lang_code, 'monitor_node')}:</b> <span id='sp-live-node'>{html.escape(node_label)}</span></div>
766
+ <div><b>{tr(lang_code, 'monitor_log')}:</b> <span id='sp-live-checkpoint-stage'>{html.escape(tr(lang_code, 'monitor_checkpoint_saved', seq=checkpoint_seq))}</span><span id='sp-live-checkpoint-seq' style='display:none'>{checkpoint_seq}</span></div>
767
+ <div id='sp-live-evidence-warning' style='display:none;margin:8px 0;padding:8px 10px;border-radius:6px;background:rgba(245,158,11,.13);'></div>
768
  <div id='sp-live-activity-feed' style='margin:8px 0 10px 0;max-height:150px;overflow:auto;line-height:1.55;'>
769
+ <div>• {html.escape(node_label)}</div>
770
  </div><hr>
771
  <div><b>{tr(lang_code, 'monitor_compute')}:</b></div>
772
  <div style='display:flex;gap:8px;'>
 
816
  "outline_planning": tr(str(st.session_state.get("sp_lang_code") or "zh"), "draft_stage_outline"),
817
  "citation_planning": tr(str(st.session_state.get("sp_lang_code") or "zh"), "draft_stage_citation"),
818
  "writing": tr(str(st.session_state.get("sp_lang_code") or "zh"), "draft_stage_writing"),
819
+ "working_front_matter": tr(str(st.session_state.get("sp_lang_code") or "zh"), "draft_stage_working_front"),
820
+ "writing_unit": tr(str(st.session_state.get("sp_lang_code") or "zh"), "draft_stage_writing_unit"),
821
+ "citation_audit_unit": tr(str(st.session_state.get("sp_lang_code") or "zh"), "draft_stage_citation_audit_unit"),
822
+ "citation_recovery_unit": tr(str(st.session_state.get("sp_lang_code") or "zh"), "draft_stage_citation_recovery_unit"),
823
+ "writing_complete_unit": tr(str(st.session_state.get("sp_lang_code") or "zh"), "draft_stage_writing_complete_unit"),
824
+ "english_validation": tr(str(st.session_state.get("sp_lang_code") or "zh"), "draft_stage_english_validation"),
825
+ "final_front_matter": tr(str(st.session_state.get("sp_lang_code") or "zh"), "draft_stage_final_front"),
826
+ "translation": tr(str(st.session_state.get("sp_lang_code") or "zh"), "draft_stage_translation"),
827
+ "packaging": tr(str(st.session_state.get("sp_lang_code") or "zh"), "draft_stage_packaging"),
828
+ "package_validation": tr(str(st.session_state.get("sp_lang_code") or "zh"), "draft_stage_package_validation"),
829
+ "package_validated": tr(str(st.session_state.get("sp_lang_code") or "zh"), "draft_stage_package_validated"),
830
+ "processing": tr(str(st.session_state.get("sp_lang_code") or "zh"), "draft_stage_processing"),
831
+ "checkpoint_saved": tr(str(st.session_state.get("sp_lang_code") or "zh"), "monitor_checkpoint_saved"),
832
+ "status_created": tr(str(st.session_state.get("sp_lang_code") or "zh"), "task_status_created"),
833
+ "status_running": tr(str(st.session_state.get("sp_lang_code") or "zh"), "task_status_running"),
834
+ "status_resumable": tr(str(st.session_state.get("sp_lang_code") or "zh"), "task_status_resumable"),
835
+ "status_cancelling": tr(str(st.session_state.get("sp_lang_code") or "zh"), "task_status_cancelling"),
836
+ "status_completed": tr(str(st.session_state.get("sp_lang_code") or "zh"), "task_status_completed"),
837
+ "status_failed": tr(str(st.session_state.get("sp_lang_code") or "zh"), "task_status_failed"),
838
+ "status_cancelled": tr(str(st.session_state.get("sp_lang_code") or "zh"), "task_status_cancelled"),
839
+ "evidence_limited": tr(str(st.session_state.get("sp_lang_code") or "zh"), "evidence_limited_running"),
840
  "progress": tr(str(st.session_state.get("sp_lang_code") or "zh"), "draft_detail_progress"),
841
  "s2": tr(str(st.session_state.get("sp_lang_code") or "zh"), "draft_detail_s2"),
842
  "oa": tr(str(st.session_state.get("sp_lang_code") or "zh"), "draft_detail_oa"),
 
1068
  f"<div id='sp-completed-download-anchor' data-sp-task-id='{html.escape(task_id, quote=True)}'></div>",
1069
  unsafe_allow_html=True,
1070
  )
1071
+ if str(task.get("safe_error_code") or "") == "W_EVIDENCE_COVERAGE_LIMITED":
1072
+ st.warning(tr(lang_code, "evidence_limited_completed"))
1073
  st.success(tr(lang_code, "success"))
1074
  st.download_button(label=tr(lang_code, "download"), data=output_path.read_bytes(), file_name=output_path.name, mime="application/zip")
1075
  _scroll_to_completed_download_once(task_id)
core/generation_runner.py CHANGED
@@ -243,6 +243,11 @@ class GenerationRunner:
243
  self._translation_progress_inflight = set()
244
  self._translation_progress_total = 0
245
  self._qualitative_quote_sources = {}
 
 
 
 
 
246
  if self.preview_token:
247
  preview_dir = Path(__file__).resolve().parents[1] / "static" / "previews"
248
  preview_dir.mkdir(parents=True, exist_ok=True)
@@ -284,9 +289,12 @@ class GenerationRunner:
284
  oa_calls=oa_calls,
285
  )
286
 
287
- def _checkpoint(self, stage: str, state: dict[str, Any], pct: int) -> None:
288
  seq, normalized_stage = self.checkpoints.save(stage, state)
289
- self._progress(pct, normalized_stage, checkpoint_seq=seq, checkpoint_stage=normalized_stage)
 
 
 
290
 
291
  @staticmethod
292
  def _preview_visible_text(value: str) -> str:
@@ -803,6 +811,7 @@ class GenerationRunner:
803
  self._preview_target_english_commit_text
804
  ),
805
  "target_progress": self._translation_progress_snapshot(),
 
806
  "updated_at": datetime.now(timezone.utc).isoformat(),
807
  }
808
  temporary = self.preview_path.with_suffix(".json.tmp")
@@ -1661,6 +1670,78 @@ class GenerationRunner:
1661
  "keywords and try again; unrelated references will not be used to satisfy a quota."
1662
  )
1663
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1664
  def _resolve_paths(self, items) -> list[str]:
1665
  output = []
1666
  for item in items or []:
@@ -1708,6 +1789,11 @@ class GenerationRunner:
1708
  else:
1709
  state = self.checkpoints.load_confirmed() or {}
1710
  quality_diagnostics = list(state.get("quality_diagnostics") or [])
 
 
 
 
 
1711
  if state:
1712
  self._preview_english_text = str(state.get('full_draft_en') or '')
1713
  self._preview_target_text = str(state.get('full_draft_translated') or '')
@@ -1879,6 +1965,22 @@ class GenerationRunner:
1879
  adaptive_structure, topic, synthesis_result, mode=mode,
1880
  target_unique=max(1, citation_plan_target),
1881
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1882
 
1883
  # Fill unit-specific reserve gaps before expensive drafting. Existing
1884
  # retrieved evidence is preferred; bounded contextual JIT is last resort.
@@ -1947,9 +2049,21 @@ class GenerationRunner:
1947
  if int(top_level_counts.get(section_id, 0)) < int(required)
1948
  ]
1949
  if shortfalls:
1950
- raise self._literature_insufficient_error(shortfalls)
 
 
 
 
 
 
 
 
 
 
 
 
1951
  logging.info(
1952
- "Top-level citation feasibility passed: minimum_total=%s reserve_target=%s planned_unique=%s sections=%s",
1953
  sum(citation_top_level_minimums.values()), citation_plan_target, len(planned_tags), len(citation_top_level_minimums),
1954
  )
1955
  canonical_concept_ledger = self._build_canonical_concept_ledger(
@@ -2145,7 +2259,10 @@ class GenerationRunner:
2145
  if int(top_level_counts.get(section_id, 0)) < int(required)
2146
  ]
2147
  if shortfalls:
2148
- raise self._literature_insufficient_error(shortfalls)
 
 
 
2149
  state["citation_plan"] = citation_plan
2150
  state["citation_minimums"] = citation_minimums
2151
  state["citation_reserve_targets"] = citation_reserve_targets
@@ -2343,7 +2460,7 @@ Output only final academic prose. Do not report the word count."""
2343
 
2344
  current_index = len(completed_sections) + 1
2345
  pct = min(88, 15 + int((current_index / max(1, total_steps)) * 72))
2346
- self._progress(pct, display_name)
2347
  prefix = ""
2348
  if sec.get("section_first"):
2349
  prefix += f"# {sec_title}\n\n"
@@ -2384,6 +2501,7 @@ Output only final academic prose. Do not report the word count."""
2384
  en_raw = agent.reinforce_claim_specific_citation_coverage(
2385
  en_raw, assigned_tag_list, section_name=display_name
2386
  )
 
2387
  used_before = set(agent.used_citations)
2388
  en_resolved = agent.audit_and_resolve_citations(
2389
  prefix + en_raw, section_name=display_name, quarantine=is_quarantined
@@ -2424,7 +2542,9 @@ Output only final academic prose. Do not report the word count."""
2424
  # an already-written claim. It is never authorized to create a
2425
  # supplemental paragraph from a source merely to satisfy citation
2426
  # survival. If no existing claim can be validated, leave prose
2427
- # unchanged and let the literature-sufficiency Gate fail closed.
 
 
2428
  recovered = agent.recover_citations_on_existing_claims(
2429
  en_resolved,
2430
  recovery_items[:needed],
@@ -2508,7 +2628,10 @@ Output only final academic prose. Do not report the word count."""
2508
  "data_metadata": data_metadata,
2509
  }
2510
  )
2511
- self._checkpoint(f"section_{len(completed_sections):03d}", state, pct)
 
 
 
2512
 
2513
  if not state.get("english_finalized"):
2514
  self._progress(91, "english_validation")
@@ -2554,13 +2677,36 @@ Output only final academic prose. Do not report the word count."""
2554
  required = min(int(planned_minimum), external_claims) if external_claims else 0
2555
  if len(validated_tags) < required:
2556
  final_shortfalls.append((section_id, len(validated_tags), required))
2557
- if final_shortfalls:
2558
- raise self._literature_insufficient_error(final_shortfalls)
2559
  final_reference_count = len(references)
2560
- logging.info(
2561
- "Top-level claim-evidence final Gate passed: validated_references=%s sections=%s",
2562
- final_reference_count, len(citation_top_level_minimums),
2563
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2564
  full_draft_en = full_draft_en.rstrip() + "\n\n# References\n" + "\n\n".join(references)
2565
  actual_words = self._english_count_for_target(full_draft_en)
2566
  self._upsert_quality_diagnostic(
@@ -2624,6 +2770,10 @@ Output only final academic prose. Do not report the word count."""
2624
  "final_keywords": final_keywords,
2625
  "front_matter_phase": "final",
2626
  "final_reference_count": final_reference_count,
 
 
 
 
2627
  "citation_plan": citation_plan,
2628
  "citation_minimums": citation_minimums,
2629
  "citation_reserve_targets": citation_reserve_targets,
@@ -2683,7 +2833,10 @@ Output only final academic prose. Do not report the word count."""
2683
  'agent_state': agent.export_state(),
2684
  })
2685
  pct = min(96, 94 + int(index * 2 / max(1, len(final_units))))
2686
- self._checkpoint(f'translation_{unit_key}', state, pct)
 
 
 
2687
 
2688
  unresolved_units = [
2689
  key for key, _name, _source in final_units
@@ -2728,7 +2881,14 @@ Output only final academic prose. Do not report the word count."""
2728
  existing_path = self.output_dir / os.path.basename(existing_output)
2729
  if existing_path.is_file() and existing_path.stat().st_size > 0:
2730
  self._progress(99, "package_validated")
2731
- return {"output_filename": existing_path.name, "output_path": str(existing_path)}
 
 
 
 
 
 
 
2732
 
2733
  self._progress(97, "packaging")
2734
  bj_time = datetime.now(timezone(timedelta(hours=8))).strftime("%Y%m%d_%H%M")
@@ -2742,6 +2902,13 @@ Output only final academic prose. Do not report the word count."""
2742
  citation_audit=agent.citation_audit_log,
2743
  literature_metrics=agent.get_source_metrics(),
2744
  quality_diagnostics=quality_diagnostics,
 
 
 
 
 
 
 
2745
  )
2746
  self._progress(98, "package_validation")
2747
  output_filename = f"SmartPaper_{bj_time}.zip"
@@ -2753,11 +2920,20 @@ Output only final academic prose. Do not report the word count."""
2753
  {
2754
  "output_filename": output_filename,
2755
  "package_validated": True,
 
 
2756
  "agent_state": agent.export_state(),
2757
  }
2758
  )
2759
- self._checkpoint("package_validated", state, 99)
2760
- return {"output_filename": output_filename, "output_path": str(final)}
 
 
 
 
 
 
 
2761
 
2762
  def run(self) -> dict[str, Any]:
2763
  try:
 
243
  self._translation_progress_inflight = set()
244
  self._translation_progress_total = 0
245
  self._qualitative_quote_sources = {}
246
+ # R14-TQ2-PQ1-R3-R1: evidence coverage is a quality signal, not a
247
+ # manuscript-completeness authority. It is persisted in checkpoints and
248
+ # exposed to the browser only as a stable status code.
249
+ self._preview_evidence_status = "normal"
250
+ self._evidence_coverage_events = []
251
  if self.preview_token:
252
  preview_dir = Path(__file__).resolve().parents[1] / "static" / "previews"
253
  preview_dir.mkdir(parents=True, exist_ok=True)
 
289
  oa_calls=oa_calls,
290
  )
291
 
292
+ def _checkpoint(self, stage: str, state: dict[str, Any], pct: int, *, display_node: str | None = None) -> None:
293
  seq, normalized_stage = self.checkpoints.save(stage, state)
294
+ self._progress(
295
+ pct, display_node or normalized_stage,
296
+ checkpoint_seq=seq, checkpoint_stage=normalized_stage,
297
+ )
298
 
299
  @staticmethod
300
  def _preview_visible_text(value: str) -> str:
 
811
  self._preview_target_english_commit_text
812
  ),
813
  "target_progress": self._translation_progress_snapshot(),
814
+ "evidence_status": str(self._preview_evidence_status or "normal")[:40],
815
  "updated_at": datetime.now(timezone.utc).isoformat(),
816
  }
817
  temporary = self.preview_path.with_suffix(".json.tmp")
 
1670
  "keywords and try again; unrelated references will not be used to satisfy a quota."
1671
  )
1672
 
1673
+ @staticmethod
1674
+ def _evidence_warning_detail() -> str:
1675
+ return (
1676
+ "Some externally oriented claims have limited direct high-relevance literature coverage. "
1677
+ "This may reflect a newer or specialized topic, dispersed literature, search/keyword limits, "
1678
+ "or limited prior evidence; this status does not determine the novelty, importance, or overall "
1679
+ "research value of the topic. Unrelated sources were not used to satisfy a citation count."
1680
+ )
1681
+
1682
+ def _record_evidence_coverage_event(self, state, shortfalls, *, phase: str, preview_status: str) -> None:
1683
+ """Persist a neutral evidence-coverage signal without stopping generation.
1684
+
1685
+ The event records only deterministic coverage counts. It never infers why
1686
+ literature is sparse and never evaluates topic value. Duplicate events
1687
+ are collapsed across resume/replay.
1688
+ """
1689
+ rows = []
1690
+ for name, actual, required in shortfalls or []:
1691
+ rows.append({
1692
+ "section": str(name or ""),
1693
+ "actual": max(0, int(actual or 0)),
1694
+ "required": max(0, int(required or 0)),
1695
+ })
1696
+ if not rows:
1697
+ return
1698
+ signature = (str(phase or ""), tuple((r["section"], r["actual"], r["required"]) for r in rows))
1699
+ existing = list(state.get("evidence_coverage_events") or self._evidence_coverage_events or [])
1700
+ seen = {
1701
+ (str(item.get("phase") or ""), tuple(
1702
+ (str(row.get("section") or ""), int(row.get("actual") or 0), int(row.get("required") or 0))
1703
+ for row in (item.get("shortfalls") or []) if isinstance(row, dict)
1704
+ ))
1705
+ for item in existing if isinstance(item, dict)
1706
+ }
1707
+ if signature not in seen:
1708
+ existing.append({
1709
+ "phase": str(phase or ""),
1710
+ "shortfalls": rows,
1711
+ "observed_at": datetime.now(timezone.utc).isoformat(),
1712
+ })
1713
+ self._evidence_coverage_events = existing
1714
+ state["evidence_coverage_events"] = existing
1715
+ state["evidence_risk_detected"] = True
1716
+ self._preview_evidence_status = str(preview_status or "limited_risk")
1717
+ self._write_preview_payload(force=True)
1718
+ logging.warning(
1719
+ "Evidence coverage warning | phase=%s | shortfalls=%s | nonfatal=true",
1720
+ phase, rows,
1721
+ )
1722
+
1723
+ @staticmethod
1724
+ def _evidence_coverage_summary(*, status, early_risk, final_reference_count, minimums, validated_by_top_level, claims_by_top_level, events):
1725
+ sections = []
1726
+ for section_id, planned_minimum in dict(minimums or {}).items():
1727
+ validated = len(set((validated_by_top_level or {}).get(section_id) or []))
1728
+ external_claims = max(0, int((claims_by_top_level or {}).get(section_id, 0) or 0))
1729
+ required = min(int(planned_minimum), external_claims) if external_claims else 0
1730
+ sections.append({
1731
+ "section": str(section_id),
1732
+ "validated_direct_citations": validated,
1733
+ "evidence_requirement": required,
1734
+ "external_claims": external_claims,
1735
+ "planning_minimum": int(planned_minimum),
1736
+ })
1737
+ return {
1738
+ "status": str(status or "adequate"),
1739
+ "early_risk_detected": bool(early_risk),
1740
+ "final_reference_count": max(0, int(final_reference_count or 0)),
1741
+ "sections": sections,
1742
+ "events": list(events or []),
1743
+ }
1744
+
1745
  def _resolve_paths(self, items) -> list[str]:
1746
  output = []
1747
  for item in items or []:
 
1789
  else:
1790
  state = self.checkpoints.load_confirmed() or {}
1791
  quality_diagnostics = list(state.get("quality_diagnostics") or [])
1792
+ self._evidence_coverage_events = list(state.get("evidence_coverage_events") or [])
1793
+ if str(state.get("evidence_status") or "") == "limited":
1794
+ self._preview_evidence_status = "limited"
1795
+ elif bool(state.get("evidence_risk_detected")):
1796
+ self._preview_evidence_status = "limited_risk"
1797
  if state:
1798
  self._preview_english_text = str(state.get('full_draft_en') or '')
1799
  self._preview_target_text = str(state.get('full_draft_translated') or '')
 
1965
  adaptive_structure, topic, synthesis_result, mode=mode,
1966
  target_unique=max(1, citation_plan_target),
1967
  )
1968
+ # Early neutral risk signal: evaluate the high-relevance plan before
1969
+ # reserve filling/JIT can make a raw candidate count look healthier
1970
+ # than the claim-specific plan actually is. This is advisory only.
1971
+ initial_top_level_counts = self._top_level_unique_tag_counts(
1972
+ citation_plan, citation_unit_section_map
1973
+ )
1974
+ initial_shortfalls = [
1975
+ (section_id, int(initial_top_level_counts.get(section_id, 0)), required)
1976
+ for section_id, required in citation_top_level_minimums.items()
1977
+ if int(initial_top_level_counts.get(section_id, 0)) < int(required)
1978
+ ]
1979
+ if initial_shortfalls:
1980
+ self._record_evidence_coverage_event(
1981
+ state, initial_shortfalls, phase="initial_claim_specific_plan",
1982
+ preview_status="limited_risk",
1983
+ )
1984
 
1985
  # Fill unit-specific reserve gaps before expensive drafting. Existing
1986
  # retrieved evidence is preferred; bounded contextual JIT is last resort.
 
2049
  if int(top_level_counts.get(section_id, 0)) < int(required)
2050
  ]
2051
  if shortfalls:
2052
+ self._record_evidence_coverage_event(
2053
+ state, shortfalls, phase="bounded_retrieval_complete",
2054
+ preview_status="limited_risk",
2055
+ )
2056
+ logging.warning(
2057
+ "Top-level citation feasibility limited: minimum_total=%s reserve_target=%s planned_unique=%s sections=%s nonfatal=true",
2058
+ sum(citation_top_level_minimums.values()), citation_plan_target, len(planned_tags), len(citation_top_level_minimums),
2059
+ )
2060
+ else:
2061
+ logging.info(
2062
+ "Top-level citation feasibility passed: minimum_total=%s reserve_target=%s planned_unique=%s sections=%s",
2063
+ sum(citation_top_level_minimums.values()), citation_plan_target, len(planned_tags), len(citation_top_level_minimums),
2064
+ )
2065
  logging.info(
2066
+ "Top-level citation planning complete: minimum_total=%s reserve_target=%s planned_unique=%s sections=%s",
2067
  sum(citation_top_level_minimums.values()), citation_plan_target, len(planned_tags), len(citation_top_level_minimums),
2068
  )
2069
  canonical_concept_ledger = self._build_canonical_concept_ledger(
 
2259
  if int(top_level_counts.get(section_id, 0)) < int(required)
2260
  ]
2261
  if shortfalls:
2262
+ self._record_evidence_coverage_event(
2263
+ state, shortfalls, phase="resume_claim_specific_plan",
2264
+ preview_status="limited_risk",
2265
+ )
2266
  state["citation_plan"] = citation_plan
2267
  state["citation_minimums"] = citation_minimums
2268
  state["citation_reserve_targets"] = citation_reserve_targets
 
2460
 
2461
  current_index = len(completed_sections) + 1
2462
  pct = min(88, 15 + int((current_index / max(1, total_steps)) * 72))
2463
+ self._progress(pct, f"writing:{current_index}:{total_steps}")
2464
  prefix = ""
2465
  if sec.get("section_first"):
2466
  prefix += f"# {sec_title}\n\n"
 
2501
  en_raw = agent.reinforce_claim_specific_citation_coverage(
2502
  en_raw, assigned_tag_list, section_name=display_name
2503
  )
2504
+ self._progress(pct, f"citation_audit:{current_index}:{total_steps}")
2505
  used_before = set(agent.used_citations)
2506
  en_resolved = agent.audit_and_resolve_citations(
2507
  prefix + en_raw, section_name=display_name, quarantine=is_quarantined
 
2542
  # an already-written claim. It is never authorized to create a
2543
  # supplemental paragraph from a source merely to satisfy citation
2544
  # survival. If no existing claim can be validated, leave prose
2545
+ # unchanged. Evidence coverage may later be reported as limited,
2546
+ # while citation correctness itself remains fail-closed.
2547
+ self._progress(pct, f"citation_recovery:{current_index}:{total_steps}")
2548
  recovered = agent.recover_citations_on_existing_claims(
2549
  en_resolved,
2550
  recovery_items[:needed],
 
2628
  "data_metadata": data_metadata,
2629
  }
2630
  )
2631
+ self._checkpoint(
2632
+ f"section_{len(completed_sections):03d}", state, pct,
2633
+ display_node=f"writing_complete:{len(completed_sections)}:{total_steps}",
2634
+ )
2635
 
2636
  if not state.get("english_finalized"):
2637
  self._progress(91, "english_validation")
 
2677
  required = min(int(planned_minimum), external_claims) if external_claims else 0
2678
  if len(validated_tags) < required:
2679
  final_shortfalls.append((section_id, len(validated_tags), required))
 
 
2680
  final_reference_count = len(references)
2681
+ if final_shortfalls:
2682
+ self._record_evidence_coverage_event(
2683
+ state, final_shortfalls, phase="final_claim_evidence_gate",
2684
+ preview_status="limited",
2685
+ )
2686
+ evidence_status = "limited"
2687
+ logging.warning(
2688
+ "Top-level claim-evidence final Gate completed with evidence warning: validated_references=%s sections=%s shortfalls=%s",
2689
+ final_reference_count, len(citation_top_level_minimums), final_shortfalls,
2690
+ )
2691
+ else:
2692
+ evidence_status = "adequate"
2693
+ self._preview_evidence_status = "adequate"
2694
+ self._write_preview_payload(force=True)
2695
+ logging.info(
2696
+ "Top-level claim-evidence final Gate passed: validated_references=%s sections=%s",
2697
+ final_reference_count, len(citation_top_level_minimums),
2698
+ )
2699
+ evidence_coverage_summary = self._evidence_coverage_summary(
2700
+ status=evidence_status,
2701
+ early_risk=bool(state.get("evidence_risk_detected")),
2702
+ final_reference_count=final_reference_count,
2703
+ minimums=citation_top_level_minimums,
2704
+ validated_by_top_level=validated_by_top_level,
2705
+ claims_by_top_level=claims_by_top_level,
2706
+ events=state.get("evidence_coverage_events") or self._evidence_coverage_events,
2707
+ )
2708
+ state["evidence_status"] = evidence_status
2709
+ state["evidence_coverage_summary"] = evidence_coverage_summary
2710
  full_draft_en = full_draft_en.rstrip() + "\n\n# References\n" + "\n\n".join(references)
2711
  actual_words = self._english_count_for_target(full_draft_en)
2712
  self._upsert_quality_diagnostic(
 
2770
  "final_keywords": final_keywords,
2771
  "front_matter_phase": "final",
2772
  "final_reference_count": final_reference_count,
2773
+ "evidence_status": str(state.get("evidence_status") or "adequate"),
2774
+ "evidence_risk_detected": bool(state.get("evidence_risk_detected")),
2775
+ "evidence_coverage_events": list(state.get("evidence_coverage_events") or []),
2776
+ "evidence_coverage_summary": dict(state.get("evidence_coverage_summary") or {}),
2777
  "citation_plan": citation_plan,
2778
  "citation_minimums": citation_minimums,
2779
  "citation_reserve_targets": citation_reserve_targets,
 
2833
  'agent_state': agent.export_state(),
2834
  })
2835
  pct = min(96, 94 + int(index * 2 / max(1, len(final_units))))
2836
+ self._checkpoint(
2837
+ f'translation_{unit_key}', state, pct,
2838
+ display_node=f'translation_final_gate:{index}:{len(final_units)}',
2839
+ )
2840
 
2841
  unresolved_units = [
2842
  key for key, _name, _source in final_units
 
2881
  existing_path = self.output_dir / os.path.basename(existing_output)
2882
  if existing_path.is_file() and existing_path.stat().st_size > 0:
2883
  self._progress(99, "package_validated")
2884
+ result = {"output_filename": existing_path.name, "output_path": str(existing_path)}
2885
+ if str(state.get("evidence_status") or "") == "limited":
2886
+ result.update({
2887
+ "quality_status": "evidence_limited",
2888
+ "safe_error_code": "W_EVIDENCE_COVERAGE_LIMITED",
2889
+ "safe_error_detail": self._evidence_warning_detail(),
2890
+ })
2891
+ return result
2892
 
2893
  self._progress(97, "packaging")
2894
  bj_time = datetime.now(timezone(timedelta(hours=8))).strftime("%Y%m%d_%H%M")
 
2902
  citation_audit=agent.citation_audit_log,
2903
  literature_metrics=agent.get_source_metrics(),
2904
  quality_diagnostics=quality_diagnostics,
2905
+ evidence_coverage=state.get("evidence_coverage_summary") or {
2906
+ "status": str(state.get("evidence_status") or "adequate"),
2907
+ "early_risk_detected": bool(state.get("evidence_risk_detected")),
2908
+ "final_reference_count": int(state.get("final_reference_count") or 0),
2909
+ "sections": [],
2910
+ "events": list(state.get("evidence_coverage_events") or []),
2911
+ },
2912
  )
2913
  self._progress(98, "package_validation")
2914
  output_filename = f"SmartPaper_{bj_time}.zip"
 
2920
  {
2921
  "output_filename": output_filename,
2922
  "package_validated": True,
2923
+ "evidence_status": str(state.get("evidence_status") or "adequate"),
2924
+ "evidence_coverage_summary": dict(state.get("evidence_coverage_summary") or {}),
2925
  "agent_state": agent.export_state(),
2926
  }
2927
  )
2928
+ self._checkpoint("package_validated", state, 99, display_node="package_validated")
2929
+ result = {"output_filename": output_filename, "output_path": str(final)}
2930
+ if str(state.get("evidence_status") or "") == "limited":
2931
+ result.update({
2932
+ "quality_status": "evidence_limited",
2933
+ "safe_error_code": "W_EVIDENCE_COVERAGE_LIMITED",
2934
+ "safe_error_detail": self._evidence_warning_detail(),
2935
+ })
2936
+ return result
2937
 
2938
  def run(self) -> dict[str, Any]:
2939
  try:
core/production_guard.py CHANGED
@@ -498,6 +498,50 @@ def build_word_count_quality_report(diagnostics: list[dict[str, Any]] | None) ->
498
  ])
499
  return "\n".join(lines).strip() + "\n"
500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
  def _manifest(entries: dict[str, bytes]) -> str:
502
  rows = []
503
  for name in sorted(entries):
@@ -516,6 +560,7 @@ def build_validated_package(
516
  citation_audit: list[dict[str, Any]],
517
  literature_metrics: dict[str, Any],
518
  quality_diagnostics: list[dict[str, Any]] | None = None,
 
519
  ) -> PackageResult:
520
  validate_markdown_manuscript(english_markdown, language_name="English")
521
  validate_markdown_manuscript(translated_markdown, language_name="Target-language")
@@ -543,6 +588,7 @@ def build_validated_package(
543
  "CITATION_AUDIT.json": json.dumps(citation_audit or [], ensure_ascii=False, indent=2).encode("utf-8"),
544
  "LITERATURE_METRICS.json": json.dumps(literature_metrics or {}, ensure_ascii=False, indent=2).encode("utf-8"),
545
  "WORD_COUNT_QUALITY_REPORT.txt": build_word_count_quality_report(quality_diagnostics).encode("utf-8"),
 
546
  }
547
  manifest = _manifest(entries)
548
  entries["FILE_MANIFEST_SHA256.txt"] = manifest.encode("utf-8")
 
498
  ])
499
  return "\n".join(lines).strip() + "\n"
500
 
501
+ def build_evidence_coverage_report(summary: dict[str, Any] | None) -> str:
502
+ """Build a neutral reader-facing literature evidence coverage report.
503
+
504
+ Sparse literature can have several explanations. This report intentionally
505
+ describes observed coverage only and never infers topic value, novelty, or
506
+ importance from literature counts.
507
+ """
508
+ data = dict(summary or {})
509
+ status = str(data.get("status") or "adequate").strip().lower()
510
+ limited = status == "limited"
511
+ lines = [
512
+ "SmartPaper Evidence Coverage Report",
513
+ "",
514
+ f"Overall evidence status: {'LIMITED' if limited else 'ADEQUATE'}",
515
+ f"Validated references in final manuscript: {max(0, int(data.get('final_reference_count') or 0))}",
516
+ f"Early coverage risk detected: {'yes' if bool(data.get('early_risk_detected')) else 'no'}",
517
+ "",
518
+ "Interpretation:",
519
+ "This status describes direct literature-evidence coverage only. It does not determine the novelty, importance, or overall research value of the topic.",
520
+ "A limited literature base may reflect a newer research area, a specialized or less-studied topic, dispersed interdisciplinary evidence, search/keyword or database coverage limits, or genuinely limited prior theoretical/empirical support.",
521
+ "SmartPaper does not infer which explanation applies from citation counts alone and does not use unrelated references merely to satisfy a quota.",
522
+ "",
523
+ ]
524
+ sections = [item for item in (data.get("sections") or []) if isinstance(item, dict)]
525
+ if sections:
526
+ lines.append("Top-level academic-section coverage:")
527
+ for item in sections:
528
+ section = str(item.get("section") or "section")
529
+ validated = max(0, int(item.get("validated_direct_citations") or 0))
530
+ required = max(0, int(item.get("evidence_requirement") or 0))
531
+ external_claims = max(0, int(item.get("external_claims") or 0))
532
+ lines.append(
533
+ f"- {section}: validated direct citations={validated}; evidence requirement={required}; externally oriented claims={external_claims}."
534
+ )
535
+ if limited:
536
+ lines.extend([
537
+ "",
538
+ "Author guidance:",
539
+ "The manuscript was completed because structural, translation, citation-correctness, quotation/fact, DOCX, and ZIP integrity gates are independent from literature coverage quantity.",
540
+ "Before formal submission, review the evidence gaps and consider adding more directly relevant core literature where appropriate.",
541
+ ])
542
+ return "\n".join(lines).strip() + "\n"
543
+
544
+
545
  def _manifest(entries: dict[str, bytes]) -> str:
546
  rows = []
547
  for name in sorted(entries):
 
560
  citation_audit: list[dict[str, Any]],
561
  literature_metrics: dict[str, Any],
562
  quality_diagnostics: list[dict[str, Any]] | None = None,
563
+ evidence_coverage: dict[str, Any] | None = None,
564
  ) -> PackageResult:
565
  validate_markdown_manuscript(english_markdown, language_name="English")
566
  validate_markdown_manuscript(translated_markdown, language_name="Target-language")
 
588
  "CITATION_AUDIT.json": json.dumps(citation_audit or [], ensure_ascii=False, indent=2).encode("utf-8"),
589
  "LITERATURE_METRICS.json": json.dumps(literature_metrics or {}, ensure_ascii=False, indent=2).encode("utf-8"),
590
  "WORD_COUNT_QUALITY_REPORT.txt": build_word_count_quality_report(quality_diagnostics).encode("utf-8"),
591
+ "EVIDENCE_COVERAGE_REPORT.txt": build_evidence_coverage_report(evidence_coverage).encode("utf-8"),
592
  }
593
  manifest = _manifest(entries)
594
  entries["FILE_MANIFEST_SHA256.txt"] = manifest.encode("utf-8")
i18n.py CHANGED
@@ -227,6 +227,18 @@ TEXTS: dict[str, dict[str, str]] = {
227
  "draft_stage_outline": "正在规划论文结构…",
228
  "draft_stage_citation": "正在建立论点与引用关系…",
229
  "draft_stage_writing": "正在准备正文写作…",
 
 
 
 
 
 
 
 
 
 
 
 
230
  "draft_detail_progress": "论文进度:{progress}%",
231
  "draft_detail_s2": "Semantic Scholar:{count} 篇有效文献",
232
  "draft_detail_oa": "OpenAlex:{count} 篇有效文献",
@@ -305,8 +317,10 @@ TEXTS: dict[str, dict[str, str]] = {
305
  "word_count": "📊 本章节实际字数:{actual}(目标:{target}),累计偏差:{deviation:+d}",
306
  "translation_running": "⏳ 英文已完成,正在后台异步翻译……",
307
  "node_error": "🚨 节点异常已中止。",
308
- "literature_insufficient": "当前高相关文献不足以直接支撑部分核心论断。系统没有使用无关文献凑数。请调整研究题目或关键词,或上传更直接相关的核心文献后重试。",
309
- "upstream_translation_stopped": "英文母稿的证据检查未通过,后续翻译已停止;当前页面中已显示的译文不是完整论文。",
 
 
310
  "node_error_detail": "错误代码:{code}|{error}|系统已保存断点,可继续恢复。",
311
  "task_cancelled": "任务已取消,断点和临时文件已删除。",
312
  "task_start_failed": "无法启动论文任务:{error}",
@@ -324,6 +338,14 @@ TEXTS: dict[str, dict[str, str]] = {
324
  "monitor_node": "📍 当前节点",
325
  "monitor_log": "📝 动作日志",
326
  "monitor_compute": "📡 三维算力消耗监控",
 
 
 
 
 
 
 
 
327
  "actual_stats_parsed": "✅ 已成功解析用户上传的实证数据。",
328
  "synthetic_stats_generated": "⚙️ 未上传统计结果,系统已生成可用于论文结构与结果写作的统计数据。",
329
  "qualitative_ready": "✅ 定性材料已准备完成。",
@@ -405,6 +427,18 @@ TEXTS: dict[str, dict[str, str]] = {
405
  "draft_stage_outline": "Planning the manuscript structure…",
406
  "draft_stage_citation": "Mapping claims to supporting sources…",
407
  "draft_stage_writing": "Preparing to draft the manuscript…",
 
 
 
 
 
 
 
 
 
 
 
 
408
  "draft_detail_progress": "Paper progress: {progress}%",
409
  "draft_detail_s2": "Semantic Scholar: {count} eligible papers",
410
  "draft_detail_oa": "OpenAlex: {count} eligible papers",
@@ -483,8 +517,10 @@ TEXTS: dict[str, dict[str, str]] = {
483
  "word_count": "📊 Section word count: {actual} (target: {target}); cumulative deviation: {deviation:+d}",
484
  "translation_running": "⏳ English draft complete. Background translation is running…",
485
  "node_error": "🚨 The node stopped because of an error.",
486
- "literature_insufficient": "The available high-relevance literature is insufficient to support some core manuscript claims. Unrelated sources were not used to fill a quota. Revise the research topic or keywords, or provide more directly relevant core literature, and try again.",
487
- "upstream_translation_stopped": "The English source manuscript did not pass the evidence gate, so downstream translation stopped. Any translated text currently shown is incomplete and is not a final paper.",
 
 
488
  "node_error_detail": "Error code: {code} | {error} | A checkpoint has been saved and can be resumed.",
489
  "task_cancelled": "The task was cancelled. Its checkpoint and temporary files were deleted.",
490
  "task_start_failed": "The paper task could not be started: {error}",
@@ -502,6 +538,14 @@ TEXTS: dict[str, dict[str, str]] = {
502
  "monitor_node": "📍 Current node",
503
  "monitor_log": "📝 Activity log",
504
  "monitor_compute": "📡 Compute-usage monitor",
 
 
 
 
 
 
 
 
505
  "actual_stats_parsed": "✅ The uploaded empirical results were parsed successfully.",
506
  "synthetic_stats_generated": "⚙️ No statistical results were uploaded. The system generated statistical data for the paper structure and Results section.",
507
  "qualitative_ready": "✅ Qualitative materials are ready.",
@@ -583,6 +627,18 @@ TEXTS: dict[str, dict[str, str]] = {
583
  "draft_stage_outline": "Планирование структуры статьи…",
584
  "draft_stage_citation": "Связывание тезисов с источниками…",
585
  "draft_stage_writing": "Подготовка к написанию текста…",
 
 
 
 
 
 
 
 
 
 
 
 
586
  "draft_detail_progress": "Прогресс статьи: {progress}%",
587
  "draft_detail_s2": "Semantic Scholar: {count} подходящих источников",
588
  "draft_detail_oa": "OpenAlex: {count} подходящих источников",
@@ -661,8 +717,10 @@ TEXTS: dict[str, dict[str, str]] = {
661
  "word_count": "📊 Объём раздела: {actual} слов (цель: {target}); накопленное отклонение: {deviation:+d}",
662
  "translation_running": "⏳ Английский текст завершён. Выполняется фоновый перевод…",
663
  "node_error": "🚨 Выполнение узла остановлено из-за ошибки.",
664
- "literature_insufficient": "Доступной высокорелевантной литературы недостаточно для прямой поддержки части ключевых утверждений рукописи. Система не использовала нерелевантные источники ради количества. Измените тему или ключевые слова либо добавьте более релевантные основные источники и повторите попытку.",
665
- "upstream_translation_stopped": "Английская исходная рукопись не прошла проверку доказательной базы, поэтому дальнейший перевод остановлен. Уже показанный перевод неполный и не является окончательной статьёй.",
 
 
666
  "node_error_detail": "Код ошибки: {code} | {error} | Контрольная точка сохранена; работу можно продолжить.",
667
  "task_cancelled": "Задача отменена. Контрольная точка и временные файлы удалены.",
668
  "task_start_failed": "Не удалось запустить подготовку статьи: {error}",
@@ -680,6 +738,14 @@ TEXTS: dict[str, dict[str, str]] = {
680
  "monitor_node": "📍 Текущий узел",
681
  "monitor_log": "📝 Журнал действий",
682
  "monitor_compute": "📡 Монитор вычислительных затрат",
 
 
 
 
 
 
 
 
683
  "actual_stats_parsed": "✅ Загруженные эмпирические результаты успешно обработаны.",
684
  "synthetic_stats_generated": "⚙️ Статистические результаты не загружены. Система сформировала статистические данные для структуры статьи и раздела результатов.",
685
  "qualitative_ready": "✅ Качественные материалы подготовлены.",
@@ -761,6 +827,18 @@ TEXTS: dict[str, dict[str, str]] = {
761
  "draft_stage_outline": "Мақала құрылымы жоспарлануда…",
762
  "draft_stage_citation": "Тұжырымдар дереккөздермен сәйкестендірілуде…",
763
  "draft_stage_writing": "Негізгі мәтінді жазуға дайындалуда…",
 
 
 
 
 
 
 
 
 
 
 
 
764
  "draft_detail_progress": "Мақала барысы: {progress}%",
765
  "draft_detail_s2": "Semantic Scholar: {count} жарамды дереккөз",
766
  "draft_detail_oa": "OpenAlex: {count} жарамды дереккөз",
@@ -839,8 +917,10 @@ TEXTS: dict[str, dict[str, str]] = {
839
  "word_count": "📊 Бөлім көлемі: {actual} сөз (мақсат: {target}); жиынтық ауытқу: {deviation:+d}",
840
  "translation_running": "⏳ Ағылшын мәтіні аяқталды. Фондық аударма орындалуда…",
841
  "node_error": "🚨 Қате салдарынан түйін тоқтатылды.",
842
- "literature_insufficient": "Қолжетімді жоғары релевантты әдебиет қолжазбаның кейбір негізгі тұжырымдарын тікелей қолдауға жеткіліксіз. Жүйе санды толтыру үшін қатысы жоқ дереккөздерді пайдаланбады. Зерттеу тақырыбын немесе кілт сөздерді өзгертіңіз не тікелей релевантты негізгі әдебиетті қосып, қайта көріңіз.",
843
- "upstream_translation_stopped": "Ағылшын тіліндегі бастапқы қолжазба дәлелдер тексерісінен өтпеді, сондықтан кейінгі аударма тоқтатылды. Қазір көрсетілген аударма толық емес және соңғы мақала болып саналмайды.",
 
 
844
  "node_error_detail": "Қате коды: {code} | {error} | Бақылау нүктесі сақталды; жұмысты жалғастыруға болады.",
845
  "task_cancelled": "Тапсырма тоқтатылды. Бақылау нүктесі мен уақытша файлдар жойылды.",
846
  "task_start_failed": "Мақала тапсырмасын бастау мүмкін болмады: {error}",
@@ -858,6 +938,14 @@ TEXTS: dict[str, dict[str, str]] = {
858
  "monitor_node": "📍 Ағымдағы түйін",
859
  "monitor_log": "📝 Әрекеттер журналы",
860
  "monitor_compute": "📡 Есептеу шығындарының мониторингі",
 
 
 
 
 
 
 
 
861
  "actual_stats_parsed": "✅ Жүктелген эмпирикалық нәтижелер сәтті өңделді.",
862
  "synthetic_stats_generated": "⚙️ Статистикалық нәтижелер жүктелмеді. Жүйе мақала құрылымы мен нәтижелер бөлімі үшін статистикалық деректер дайындады.",
863
  "qualitative_ready": "✅ Сапалық материалдар дайындалды.",
 
227
  "draft_stage_outline": "正在规划论文结构…",
228
  "draft_stage_citation": "正在建立论点与引用关系…",
229
  "draft_stage_writing": "正在准备正文写作…",
230
+ "draft_stage_working_front": "正在生成工作摘要与关键词…",
231
+ "draft_stage_writing_unit": "正在撰写英文正文:第 {current}/{total} 个写作单元",
232
+ "draft_stage_citation_audit_unit": "正在核验第 {current}/{total} 个写作单元的引用与论点匹配",
233
+ "draft_stage_citation_recovery_unit": "正在为第 {current}/{total} 个写作单元补充可直接支撑现有论点的文献",
234
+ "draft_stage_writing_complete_unit": "英文正文第 {current}/{total} 个写作单元已完成",
235
+ "draft_stage_english_validation": "正在进行英文母稿结构、事实与证据完整性检查…",
236
+ "draft_stage_final_front": "正在生成最终摘要与关键词…",
237
+ "draft_stage_translation": "英文母稿已冻结,正在完成目标语言翻译与最终质检…",
238
+ "draft_stage_packaging": "正在生成英文与目标语言 DOCX 并打包…",
239
+ "draft_stage_package_validation": "正在校验 DOCX、ZIP 与文件完整性…",
240
+ "draft_stage_package_validated": "交付文件已通过完整性检查",
241
+ "draft_stage_processing": "系统正在处理当前论文任务…",
242
  "draft_detail_progress": "论文进度:{progress}%",
243
  "draft_detail_s2": "Semantic Scholar:{count} 篇有效文献",
244
  "draft_detail_oa": "OpenAlex:{count} 篇有效文献",
 
317
  "word_count": "📊 本章节实际字数:{actual}(目标:{target}),累计偏差:{deviation:+d}",
318
  "translation_running": "⏳ 英文已完成,正在后台异步翻译……",
319
  "node_error": "🚨 节点异常已中止。",
320
+ "literature_insufficient": "当前没有检索到可用于形成有效参考文献列表的直接相关文献。系统未使用无关文献凑数。请检查研究题目、关键词或补充核心文献后重试。",
321
+ "upstream_translation_stopped": "英文母稿未形成可交付的有效参考文献列表,因此后续翻译已停止;当前页面中已显示的译文不是完整论文。",
322
+ "evidence_limited_running": "文献证据覆盖提示:当前检索与语义核验显示,部分核心外部论断可直接匹配的高相关文献较少。这可能与研究主题较新、领域较专门、跨领域文献分散、关键词表达或数据库覆盖等因素有关;仅凭这一状态不能判断研究价值。系统将继续生成论文,并且不会使用无关文献补足数量。",
323
+ "evidence_limited_completed": "论文已完整生成。部分核心外部论断的直接文献证据覆盖有限,系统未使用不相关文献凑数。该状态不能判断研究价值;建议正式投稿前结合 EVIDENCE_COVERAGE_REPORT.txt 进一步补充或复核相关核心文献。",
324
  "node_error_detail": "错误代码:{code}|{error}|系统已保存断点,可继续恢复。",
325
  "task_cancelled": "任务已取消,断点和临时文件已删除。",
326
  "task_start_failed": "无法启动论文任务:{error}",
 
338
  "monitor_node": "📍 当前节点",
339
  "monitor_log": "📝 动作日志",
340
  "monitor_compute": "📡 三维算力消耗监控",
341
+ "monitor_checkpoint_saved": "已安全保存检查点 {seq}",
342
+ "task_status_created": "已创建",
343
+ "task_status_running": "运行中",
344
+ "task_status_resumable": "可继续",
345
+ "task_status_cancelling": "正在取消",
346
+ "task_status_completed": "已完成",
347
+ "task_status_failed": "失败",
348
+ "task_status_cancelled": "已取消",
349
  "actual_stats_parsed": "✅ 已成功解析用户上传的实证数据。",
350
  "synthetic_stats_generated": "⚙️ 未上传统计结果,系统已生成可用于论文结构与结果写作的统计数据。",
351
  "qualitative_ready": "✅ 定性材料已准备完成。",
 
427
  "draft_stage_outline": "Planning the manuscript structure…",
428
  "draft_stage_citation": "Mapping claims to supporting sources…",
429
  "draft_stage_writing": "Preparing to draft the manuscript…",
430
+ "draft_stage_working_front": "Generating the Working Abstract and keywords…",
431
+ "draft_stage_writing_unit": "Writing the English manuscript: unit {current}/{total}",
432
+ "draft_stage_citation_audit_unit": "Checking claim-citation fit for writing unit {current}/{total}",
433
+ "draft_stage_citation_recovery_unit": "Finding direct support for existing claims in writing unit {current}/{total}",
434
+ "draft_stage_writing_complete_unit": "English writing unit {current}/{total} completed",
435
+ "draft_stage_english_validation": "Checking English-manuscript structure, facts, and evidence integrity…",
436
+ "draft_stage_final_front": "Generating the final Abstract and keywords…",
437
+ "draft_stage_translation": "The English source is frozen; completing target-language translation and final QA…",
438
+ "draft_stage_packaging": "Generating English and target-language DOCX files and packaging them…",
439
+ "draft_stage_package_validation": "Validating DOCX, ZIP, and file integrity…",
440
+ "draft_stage_package_validated": "Delivery files passed integrity validation",
441
+ "draft_stage_processing": "The system is processing the current paper task…",
442
  "draft_detail_progress": "Paper progress: {progress}%",
443
  "draft_detail_s2": "Semantic Scholar: {count} eligible papers",
444
  "draft_detail_oa": "OpenAlex: {count} eligible papers",
 
517
  "word_count": "📊 Section word count: {actual} (target: {target}); cumulative deviation: {deviation:+d}",
518
  "translation_running": "⏳ English draft complete. Background translation is running…",
519
  "node_error": "🚨 The node stopped because of an error.",
520
+ "literature_insufficient": "No directly relevant literature was retrieved that can support a valid final reference list. Unrelated sources were not used to fill a quota. Check the topic or keywords, or provide relevant core literature, and try again.",
521
+ "upstream_translation_stopped": "The English source manuscript could not form a deliverable validated reference list, so downstream translation stopped. Any translated text currently shown is incomplete and is not a final paper.",
522
+ "evidence_limited_running": "Literature-evidence coverage notice: current retrieval and semantic validation indicate that some core external claims have relatively few directly matching high-relevance sources. This may reflect a newer topic, a specialized field, dispersed interdisciplinary literature, keyword phrasing, or database coverage; this status alone does not determine research value. Generation will continue, and unrelated sources will not be used to fill a quota.",
523
+ "evidence_limited_completed": "The paper was generated in full. Direct literature-evidence coverage is limited for some core external claims, and unrelated sources were not used to fill a quota. This status does not determine research value. Before formal submission, review EVIDENCE_COVERAGE_REPORT.txt and consider adding or rechecking relevant core literature.",
524
  "node_error_detail": "Error code: {code} | {error} | A checkpoint has been saved and can be resumed.",
525
  "task_cancelled": "The task was cancelled. Its checkpoint and temporary files were deleted.",
526
  "task_start_failed": "The paper task could not be started: {error}",
 
538
  "monitor_node": "📍 Current node",
539
  "monitor_log": "📝 Activity log",
540
  "monitor_compute": "📡 Compute-usage monitor",
541
+ "monitor_checkpoint_saved": "Checkpoint {seq} saved safely",
542
+ "task_status_created": "Created",
543
+ "task_status_running": "Running",
544
+ "task_status_resumable": "Ready to resume",
545
+ "task_status_cancelling": "Cancelling",
546
+ "task_status_completed": "Completed",
547
+ "task_status_failed": "Failed",
548
+ "task_status_cancelled": "Cancelled",
549
  "actual_stats_parsed": "✅ The uploaded empirical results were parsed successfully.",
550
  "synthetic_stats_generated": "⚙️ No statistical results were uploaded. The system generated statistical data for the paper structure and Results section.",
551
  "qualitative_ready": "✅ Qualitative materials are ready.",
 
627
  "draft_stage_outline": "Планирование структуры статьи…",
628
  "draft_stage_citation": "Связывание тезисов с источниками…",
629
  "draft_stage_writing": "Подготовка к написанию текста…",
630
+ "draft_stage_working_front": "Формируются рабочая аннотация и ключевые слова…",
631
+ "draft_stage_writing_unit": "Подготовка английской рукописи: блок {current}/{total}",
632
+ "draft_stage_citation_audit_unit": "Проверяется соответствие утверждений и источников в блоке {current}/{total}",
633
+ "draft_stage_citation_recovery_unit": "Подбираются прямые источники для уже существующих утверждений в блоке {current}/{total}",
634
+ "draft_stage_writing_complete_unit": "Блок английской рукописи {current}/{total} завершён",
635
+ "draft_stage_english_validation": "Проверяются структура, факты и доказательная целостность английской рукописи…",
636
+ "draft_stage_final_front": "Формируются окончательная аннотация и ключевые слова…",
637
+ "draft_stage_translation": "Английская рукопись зафиксирована; завершаются перевод и итоговая проверка качества…",
638
+ "draft_stage_packaging": "Создаются английский и целевой DOCX и формируется пакет…",
639
+ "draft_stage_package_validation": "Проверяется целостность DOCX, ZIP и файлов…",
640
+ "draft_stage_package_validated": "Файлы выдачи прошли проверку целостности",
641
+ "draft_stage_processing": "Система обрабатывает текущую задачу подготовки статьи…",
642
  "draft_detail_progress": "Прогресс статьи: {progress}%",
643
  "draft_detail_s2": "Semantic Scholar: {count} подходящих источников",
644
  "draft_detail_oa": "OpenAlex: {count} подходящих источников",
 
717
  "word_count": "📊 Объём раздела: {actual} слов (цель: {target}); накопленное отклонение: {deviation:+d}",
718
  "translation_running": "⏳ Английский текст завершён. Выполняется фоновый перевод…",
719
  "node_error": "🚨 Выполнение узла остановлено из-за ошибки.",
720
+ "literature_insufficient": "Не удалось найти прямо релевантную литературу, достаточную для формирования валидного итогового списка источников. Система не использовала нерелевантные публикации ради количества. Проверьте тему и ключевые слова либо добавьте релевантные основные источники и повторите попытку.",
721
+ "upstream_translation_stopped": "Для английской исходной рукописи не удалось сформировать пригодный к выдаче валидный список источников, поэтому дальнейший перевод остановлен. Уже показанный перевод неполный и не является окончательной статьёй.",
722
+ "evidence_limited_running": "Уведомление о покрытии литературными доказательствами: текущий поиск и семантическая проверка показывают, что для части ключевых внешних утверждений найдено относительно мало высокорелевантных источников с прямой поддержкой. Это может быть связано с новизной темы, узкой специализацией, рассредоточенностью междисциплинарной литературы, формулировкой ключевых слов или охватом баз данных; по одному этому признаку нельзя судить о ценности исследования. Генерация продолжится, а нерелевантные источники не будут использоваться ради количества.",
723
+ "evidence_limited_completed": "Статья полностью сформирована. Для части ключевых внешних утверждений прямое покрытие литературными доказательствами ограничено; нерелевантные источники не использовались ради количества. Этот статус не определяет ценность исследования. Перед подачей рекомендуется изучить EVIDENCE_COVERAGE_REPORT.txt и при необходимости дополнить или перепроверить основную литературу.",
724
  "node_error_detail": "Код ошибки: {code} | {error} | Контрольная точка сохранена; работу можно продолжить.",
725
  "task_cancelled": "Задача отменена. Контрольная точка и временные файлы удалены.",
726
  "task_start_failed": "Не удалось запустить подготовку статьи: {error}",
 
738
  "monitor_node": "📍 Текущий узел",
739
  "monitor_log": "📝 Журнал действий",
740
  "monitor_compute": "📡 Монитор вычислительных затрат",
741
+ "monitor_checkpoint_saved": "Контрольная точка {seq} безопасно сохранена",
742
+ "task_status_created": "Создано",
743
+ "task_status_running": "Выполняется",
744
+ "task_status_resumable": "Можно продолжить",
745
+ "task_status_cancelling": "Отмена выполняется",
746
+ "task_status_completed": "Завершено",
747
+ "task_status_failed": "Ошибка",
748
+ "task_status_cancelled": "Отменено",
749
  "actual_stats_parsed": "✅ Загруженные эмпирические результаты успешно обработаны.",
750
  "synthetic_stats_generated": "⚙️ Статистические результаты не загружены. Система сформировала статистические данные для структуры статьи и раздела результатов.",
751
  "qualitative_ready": "✅ Качественные материалы подготовлены.",
 
827
  "draft_stage_outline": "Мақала құрылымы жоспарлануда…",
828
  "draft_stage_citation": "Тұжырымдар дереккөздермен сәйкестендірілуде…",
829
  "draft_stage_writing": "Негізгі мәтінді жазуға дайындалуда…",
830
+ "draft_stage_working_front": "Жұмыс аңдатпасы мен түйін сөздер әзірленуде…",
831
+ "draft_stage_writing_unit": "Ағылшын қолжазбасы жазылуда: {current}/{total} жазу бірлігі",
832
+ "draft_stage_citation_audit_unit": "{current}/{total} жазу бірлігіндегі тұжырымдар мен сілтемелер сәйкестігі тексерілуде",
833
+ "draft_stage_citation_recovery_unit": "{current}/{total} жазу бірлігіндегі бар тұжырымдарға тікелей қолдау көрсететін әдебиет ізделуде",
834
+ "draft_stage_writing_complete_unit": "Ағылшын қолжазбасының {current}/{total} жазу бірлігі аяқталды",
835
+ "draft_stage_english_validation": "Ағылшын қолжазбасының құрылымы, фактілері және дәлел тұтастығы тексерілуде…",
836
+ "draft_stage_final_front": "Соңғы аңдатпа мен түйін сөздер әзірленуде…",
837
+ "draft_stage_translation": "Ағылшын бастапқы мәтіні бекітілді; мақсатты тілдегі аударма және соңғы сапа тексерісі аяқталуда…",
838
+ "draft_stage_packaging": "Ағылшын және мақсатты тілдегі DOCX файлдары жасалып, пакетке жиналуда…",
839
+ "draft_stage_package_validation": "DOCX, ZIP және файл тұтастығы тексерілуде…",
840
+ "draft_stage_package_validated": "Жеткізу файлдары тұтастық тексерісінен өтті",
841
+ "draft_stage_processing": "Жүйе ағымдағы мақала тапсырмасын өңдеп жатыр…",
842
  "draft_detail_progress": "Мақала барысы: {progress}%",
843
  "draft_detail_s2": "Semantic Scholar: {count} жарамды дереккөз",
844
  "draft_detail_oa": "OpenAlex: {count} жарамды дереккөз",
 
917
  "word_count": "📊 Бөлім көлемі: {actual} сөз (мақсат: {target}); жиынтық ауытқу: {deviation:+d}",
918
  "translation_running": "⏳ Ағылшын мәтіні аяқталды. Фондық аударма орындалуда…",
919
  "node_error": "🚨 Қате салдарынан түйін тоқтатылды.",
920
+ "literature_insufficient": "Жарамды соңғы әдебиеттер тізімін құруға жеткілікті тікелей релевантты әдебиет табылмады. Жүйе санды толтыру үшін қатысы жоқ дереккөздерді пайдаланбады. Тақырып пен кілт сөздерді тексеріңіз немесе релевантты негізгі әдебиетті қосып, қайта көріңіз.",
921
+ "upstream_translation_stopped": "Ағылшын бастапқы қолжазбасы үшін жеткізуге жарамды тексерілген әдебиеттер тізімі қалыптаспады, сондықтан кейінгі аударма тоқтатылды. Қазір көрсетілген аударма толық емес және соңғы мақала болып саналмайды.",
922
+ "evidence_limited_running": "Әдеби дәлелдерді қамту туралы ескерту: ағымдағы іздеу және семантикалық тексеру кейбір негізгі сыртқы тұжырымдарға тікелей сәйкес келетін жоғары релевантты дереккөздер салыстырмалы түрде аз екенін көрсетеді. Бұл тақырыптың жаңалығына, саланың тар мамандандырылуына, пәнаралық әдебиеттің шашыраңқылығына, кілт сөздердің тұжырымдалуына немесе дерекқор қамтуына байланысты болуы мүмкін; осы мәртебенің өзі зерттеу құндылығын анықтамайды. Генерация жалғасады және жүйе санды толтыру үшін қатысы жоқ дереккөздерді пайдаланбайды.",
923
+ "evidence_limited_completed": "Мақала толық жасалды. Кейбір негізгі сыртқы тұжырымдар үшін тікелей әдеби дәлелдер қамтуы шектеулі, ал жүйе санды толтыру үшін қатысы жоқ дереккөздерді пайдаланбады. Бұл мәртебе зерттеу құндылығын анықтамайды. Ресми тапсыру алдында EVIDENCE_COVERAGE_REPORT.txt файлын қарап, қажет болса релевантты негізгі әдебиетті толықтыру немесе қайта тексеру ұсынылады.",
924
  "node_error_detail": "Қате коды: {code} | {error} | Бақылау нүктесі сақталды; жұмысты жалғастыруға болады.",
925
  "task_cancelled": "Тапсырма тоқтатылды. Бақылау нүктесі мен уақытша файлдар жойылды.",
926
  "task_start_failed": "Мақала тапсырмасын бастау мүмкін болмады: {error}",
 
938
  "monitor_node": "📍 Ағымдағы түйін",
939
  "monitor_log": "📝 Әрекеттер журналы",
940
  "monitor_compute": "📡 Есептеу шығындарының мониторингі",
941
+ "monitor_checkpoint_saved": "{seq} бақылау нүктесі қауіпсіз сақталды",
942
+ "task_status_created": "Құрылды",
943
+ "task_status_running": "Орындалуда",
944
+ "task_status_resumable": "Жалғастыруға болады",
945
+ "task_status_cancelling": "Тоқтатылуда",
946
+ "task_status_completed": "Аяқталды",
947
+ "task_status_failed": "Қате",
948
+ "task_status_cancelled": "Тоқтатылды",
949
  "actual_stats_parsed": "✅ Жүктелген эмпирикалық нәтижелер сәтті өңделді.",
950
  "synthetic_stats_generated": "⚙️ Статистикалық нәтижелер жүктелмеді. Жүйе мақала құрылымы мен нәтижелер бөлімі үшін статистикалық деректер дайындады.",
951
  "qualitative_ready": "✅ Сапалық материалдар дайындалды.",
smartpaper_restore_bridge/frontend/index.html CHANGED
@@ -315,6 +315,66 @@
315
  return output;
316
  }
317
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  function updateDraftPreparation(parentDocument, node, labels, task=null) {
319
  const status = parentDocument.getElementById("sp-live-manuscript-status");
320
  const channel = renderChannels.english;
@@ -328,7 +388,7 @@
328
  const progress = Math.max(0, Math.min(100, asCount(task && task.progress_pct)));
329
  const s2 = asCount(task && task.s2_calls);
330
  const oa = asCount(task && task.oa_calls);
331
- const stage = String(map[node] || map.writing || map.default || node || "").trim();
332
  const lines = [String(map.default || "").trim()];
333
  if (stage) lines.push(formatTemplate(map.current || "{stage}", {stage}));
334
  if (map.progress) lines.push(formatTemplate(map.progress, {progress}));
@@ -617,6 +677,7 @@
617
  lastTargetPreviewText = targetText;
618
  }
619
  updateTargetPreparation(parentDocument, payload, watch && watch.targetStatusLabels);
 
620
  }
621
 
622
  async function fetchPreview(watch) {
@@ -643,24 +704,34 @@
643
  const progress = Math.max(0, Math.min(100, asCount(task && task.progress_pct)));
644
  const checkpointSeq = asCount(task && task.checkpoint_seq);
645
  const checkpointStage = String(task && task.checkpoint_stage || "");
646
- updateText(parentDocument, "sp-live-status", status);
647
- updateText(parentDocument, "sp-live-node", node);
 
 
 
648
  updateText(parentDocument, "sp-live-checkpoint-seq", checkpointSeq);
649
- updateText(parentDocument, "sp-live-checkpoint-stage", checkpointStage);
 
 
 
650
  updateText(parentDocument, "sp-live-ds", asCount(task && task.ds_calls));
651
  updateText(parentDocument, "sp-live-s2", asCount(task && task.s2_calls));
652
  updateText(parentDocument, "sp-live-oa", asCount(task && task.oa_calls));
653
- updateText(parentDocument, "sp-live-progress-text", `${progress}% — ${node}`);
654
  const fill = parentDocument.getElementById("sp-live-progress-fill");
655
  if (fill) fill.style.width = `${progress}%`;
656
- updateDraftPreparation(parentDocument, node, watch && watch.draftStatusLabels, task);
657
 
658
  if (node && node !== lastMonitorNode) {
659
- appendTaskActivity(parentDocument, node, false);
660
  lastMonitorNode = node;
661
  }
662
  if (checkpointSeq > lastMonitorCheckpointSeq) {
663
- appendTaskActivity(parentDocument, checkpointStage || node, true);
 
 
 
 
664
  lastMonitorCheckpointSeq = checkpointSeq;
665
  }
666
  }
 
315
  return output;
316
  }
317
 
318
+ function localizeDraftNode(node, labels) {
319
+ const value = String(node || "").trim();
320
+ const map = labels && typeof labels === "object" ? labels : {};
321
+ const exact = {
322
+ "inputs_and_evidence":"inputs_and_evidence",
323
+ "S2 + OpenAlex":"S2 + OpenAlex",
324
+ "literature_synthesis":"literature_synthesis",
325
+ "research_logic":"research_logic",
326
+ "working_front_matter":"working_front_matter",
327
+ "outline_planning":"outline_planning",
328
+ "citation_planning":"citation_planning",
329
+ "english_validation":"english_validation",
330
+ "final_front_matter":"final_front_matter",
331
+ "translation":"translation",
332
+ "translation_complete":"translation",
333
+ "packaging":"packaging",
334
+ "package_validation":"package_validation",
335
+ "package_validated":"package_validated"
336
+ };
337
+ if (Object.prototype.hasOwnProperty.call(exact, value)) {
338
+ return String(map[exact[value]] || map.processing || map.default || "");
339
+ }
340
+ const typed = [
341
+ ["writing:", "writing_unit"],
342
+ ["citation_audit:", "citation_audit_unit"],
343
+ ["citation_recovery:", "citation_recovery_unit"],
344
+ ["writing_complete:", "writing_complete_unit"]
345
+ ];
346
+ for (const [prefix, key] of typed) {
347
+ if (!value.startsWith(prefix)) continue;
348
+ const parts = value.slice(prefix.length).split(":");
349
+ return formatTemplate(map[key] || map.processing || map.default || "", {
350
+ current:parts[0] || "", total:parts[1] || ""
351
+ });
352
+ }
353
+ if (value.startsWith("translation_")) return String(map.translation || map.processing || map.default || "");
354
+ if (value.startsWith("section_")) return String(map.writing || map.processing || map.default || "");
355
+ return String(map.processing || map.default || "");
356
+ }
357
+
358
+ function localizeTaskStatus(status, labels) {
359
+ const map = labels && typeof labels === "object" ? labels : {};
360
+ const key = `status_${String(status || "").trim().toLowerCase()}`;
361
+ return String(map[key] || map.processing || map.default || "");
362
+ }
363
+
364
+ function updateEvidenceWarning(parentDocument, payload, labels) {
365
+ const node = parentDocument.getElementById("sp-live-evidence-warning");
366
+ if (!node) return;
367
+ const status = String(payload && payload.evidence_status || "").trim().toLowerCase();
368
+ const map = labels && typeof labels === "object" ? labels : {};
369
+ if (status === "limited_risk" || status === "limited") {
370
+ node.textContent = String(map.evidence_limited || "");
371
+ node.style.display = node.textContent ? "block" : "none";
372
+ } else {
373
+ node.textContent = "";
374
+ node.style.display = "none";
375
+ }
376
+ }
377
+
378
  function updateDraftPreparation(parentDocument, node, labels, task=null) {
379
  const status = parentDocument.getElementById("sp-live-manuscript-status");
380
  const channel = renderChannels.english;
 
388
  const progress = Math.max(0, Math.min(100, asCount(task && task.progress_pct)));
389
  const s2 = asCount(task && task.s2_calls);
390
  const oa = asCount(task && task.oa_calls);
391
+ const stage = localizeDraftNode(node, map);
392
  const lines = [String(map.default || "").trim()];
393
  if (stage) lines.push(formatTemplate(map.current || "{stage}", {stage}));
394
  if (map.progress) lines.push(formatTemplate(map.progress, {progress}));
 
677
  lastTargetPreviewText = targetText;
678
  }
679
  updateTargetPreparation(parentDocument, payload, watch && watch.targetStatusLabels);
680
+ updateEvidenceWarning(parentDocument, payload, watch && watch.draftStatusLabels);
681
  }
682
 
683
  async function fetchPreview(watch) {
 
704
  const progress = Math.max(0, Math.min(100, asCount(task && task.progress_pct)));
705
  const checkpointSeq = asCount(task && task.checkpoint_seq);
706
  const checkpointStage = String(task && task.checkpoint_stage || "");
707
+ const labels = watch && watch.draftStatusLabels;
708
+ const statusLabel = localizeTaskStatus(status, labels);
709
+ const nodeLabel = localizeDraftNode(node, labels);
710
+ updateText(parentDocument, "sp-live-status", statusLabel);
711
+ updateText(parentDocument, "sp-live-node", nodeLabel);
712
  updateText(parentDocument, "sp-live-checkpoint-seq", checkpointSeq);
713
+ updateText(
714
+ parentDocument, "sp-live-checkpoint-stage",
715
+ formatTemplate((labels && labels.checkpoint_saved) || "", {seq:checkpointSeq})
716
+ );
717
  updateText(parentDocument, "sp-live-ds", asCount(task && task.ds_calls));
718
  updateText(parentDocument, "sp-live-s2", asCount(task && task.s2_calls));
719
  updateText(parentDocument, "sp-live-oa", asCount(task && task.oa_calls));
720
+ updateText(parentDocument, "sp-live-progress-text", `${progress}% — ${nodeLabel}`);
721
  const fill = parentDocument.getElementById("sp-live-progress-fill");
722
  if (fill) fill.style.width = `${progress}%`;
723
+ updateDraftPreparation(parentDocument, node, labels, task);
724
 
725
  if (node && node !== lastMonitorNode) {
726
+ appendTaskActivity(parentDocument, nodeLabel, false);
727
  lastMonitorNode = node;
728
  }
729
  if (checkpointSeq > lastMonitorCheckpointSeq) {
730
+ appendTaskActivity(
731
+ parentDocument,
732
+ formatTemplate((labels && labels.checkpoint_saved) || "", {seq:checkpointSeq}),
733
+ true
734
+ );
735
  lastMonitorCheckpointSeq = checkpointSeq;
736
  }
737
  }
task_executor.py CHANGED
@@ -584,6 +584,12 @@ def main() -> int:
584
  "status": "completed",
585
  "current_node": "package_validated",
586
  "output_filename": str(result.get("output_filename") or "")[:240],
 
 
 
 
 
 
587
  }
588
  )
589
  logging.info("[SP_EXECUTOR] completed task_id=%s epoch=%s", args.task_id, epoch)
 
584
  "status": "completed",
585
  "current_node": "package_validated",
586
  "output_filename": str(result.get("output_filename") or "")[:240],
587
+ # R14-TQ2-PQ1-R3-R1: completed tasks may carry a nonfatal,
588
+ # backend-trusted quality warning. Worker/D1 already persists
589
+ # safe_error_code/detail for completed terminal transitions;
590
+ # quota semantics remain the ordinary completed/committed path.
591
+ "safe_error_code": str(result.get("safe_error_code") or "")[:80],
592
+ "safe_error_detail": str(result.get("safe_error_detail") or "")[:800],
593
  }
594
  )
595
  logging.info("[SP_EXECUTOR] completed task_id=%s epoch=%s", args.task_id, epoch)