Spaces:
Running
Running
R14-TQ2-PQ1-R3-R1 Evidence-Limited Completion + Four-Language Detailed Progress
#64
by GuXiaopeng - opened
- app.py +77 -5
- core/generation_runner.py +194 -18
- core/production_guard.py +46 -0
- i18n.py +96 -8
- smartpaper_restore_bridge/frontend/index.html +79 -8
- task_executor.py +6 -0
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}% — {
|
| 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'>{
|
| 717 |
-
<div><b>{tr(lang_code, 'monitor_node')}:</b> <span id='sp-live-node'>{
|
| 718 |
-
<div><b>{tr(lang_code, 'monitor_log')}:</b>
|
|
|
|
| 719 |
<div id='sp-live-activity-feed' style='margin:8px 0 10px 0;max-height:150px;overflow:auto;line-height:1.55;'>
|
| 720 |
-
<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(
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1951 |
logging.info(
|
| 1952 |
-
"Top-level citation
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 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
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 2561 |
-
|
| 2562 |
-
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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": "
|
| 487 |
-
"upstream_translation_stopped": "The English source manuscript
|
|
|
|
|
|
|
| 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 =
|
| 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 |
-
|
| 647 |
-
|
|
|
|
|
|
|
|
|
|
| 648 |
updateText(parentDocument, "sp-live-checkpoint-seq", checkpointSeq);
|
| 649 |
-
updateText(
|
|
|
|
|
|
|
|
|
|
| 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}% — ${
|
| 654 |
const fill = parentDocument.getElementById("sp-live-progress-fill");
|
| 655 |
if (fill) fill.style.width = `${progress}%`;
|
| 656 |
-
updateDraftPreparation(parentDocument, node,
|
| 657 |
|
| 658 |
if (node && node !== lastMonitorNode) {
|
| 659 |
-
appendTaskActivity(parentDocument,
|
| 660 |
lastMonitorNode = node;
|
| 661 |
}
|
| 662 |
if (checkpointSeq > lastMonitorCheckpointSeq) {
|
| 663 |
-
appendTaskActivity(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|