SmartPaper / core /generation_runner.py
GuXiaopeng's picture
P0-R1.1-R2-R1 Evidence-Sealed Finalization
c6930c5 verified
Raw History Blame
189 kB
# -*- coding: utf-8 -*-
"""R3-Free headless SmartPaper generation runner.
The runner contains no Streamlit calls. It is safe to execute in a subprocess
whose lifetime is independent from Streamlit ScriptRunner reruns and browser
connections. Checkpoints are task-scoped, immutable-by-sequence JSON snapshots.
"""
from __future__ import annotations
import hashlib
import inspect
import json
import logging
import math
import os
import queue
import re
import threading
import time
from concurrent.futures import Future
from datetime import datetime, timezone, timedelta
from pathlib import Path
from typing import Callable, Any
from config import APP_CONFIG, AMJ_PROMPTS, ASQ_PROMPTS
from core.checkpoint_store import TaskCheckpointStore
from core.meta_extractor import extract_golden_metadata
from core.pipeline_agent import SSciPipelineAgent, PipelineCancelledError
from core.production_guard import build_validated_package, replace_abstract
from core.qualitative_engine import get_qualitative_engine
from core.quantitative_engine import get_quantitative_engine
from core.rag_engine import initialize_rag
from utils.file_parser import extract_text_from_files
ProgressCallback = Callable[..., None]
CITATION_SEMANTIC_SURVIVAL_RATIO = 0.78
CITATION_ROLE_RULES = {
"introduction": (3, 180),
"theory": (3, 180),
"theoretical_framing": (3, 180),
"methodology": (2, 280),
"methods": (2, 280),
"discussion": (2, 220),
}
TRANSLATION_CONTRACT_VERSION = 'hierarchical-multilingual-v8'
FINAL_CITATION_LEDGER_VERSION = 'resolved-final-ledger-v1'
ENGLISH_SECTION_SEAL_VERSION = 'english-section-integrity-seal-v1'
def _clean_trailing_references(text, keywords=("reference", "references", "reference list", "参考文献")):
lines = str(text or "").split("\n")
for i in range(len(lines) - 1, max(-1, len(lines) - 100), -1):
if any(re.match(rf"^##?\s*{kw}", lines[i], re.IGNORECASE) for kw in keywords):
return "\n".join(lines[:i]).strip()
return str(text or "").strip()
def _clean_markdown_headings(text):
return re.sub(r"(?m)^#+\s+(.*)$", r"**\1**", str(text or ""))
def _canonical_request_hash(request_data: dict[str, Any]) -> str:
material = dict(request_data or {})
material.pop("request_hash", None)
encoded = json.dumps(material, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
return hashlib.sha256(encoded).hexdigest()
def _normalize_target_manuscript_typography(text: str, target_lang: str) -> str:
"""Apply only deterministic language-local typography after semantic QA.
R14-TQ2-PQ1-R1 fixes English-style spaces around Chinese full-width
punctuation without touching Russian/Kazakh spacing, references, semantic
content, or code-fenced material. This runs only after the target manuscript
has passed translation QA; it is presentation normalization, not translation.
"""
value = str(text or "")
language = str(target_lang or "")
if "chinese" not in language.casefold() and "中文" not in language:
return value
value = value.replace("\r\n", "\n").replace("\r", "\n")
# The English References tail is immutable data, not target-language prose.
# Chinese punctuation normalization therefore stops before References.
reference_match = re.search(r"(?m)^#\s+参考文献\s*$", value)
frozen_reference_tail = ""
if reference_match:
frozen_reference_tail = value[reference_match.start():]
value = value[:reference_match.start()]
lines = value.split("\n")
normalized = []
in_fence = False
for line in lines:
stripped = line.lstrip()
if stripped.startswith("```"):
in_fence = not in_fence
normalized.append(line)
continue
if in_fence:
normalized.append(line)
continue
current = re.sub(r"[ \t]+([,。!?;:])", r"\1", line)
current = re.sub(r"([,。!?;:])[ \t]+(?=\S)", r"\1", current)
normalized.append(current)
normalized_body = "\n".join(normalized)
return normalized_body + frozen_reference_tail
class _OrderedTranslationWorker:
"""One daemon worker owned by the headless Executor process.
It intentionally has no checkpoint authority. Results are returned through
Future objects and are checkpointed only by GenerationRunner's main thread.
"""
def __init__(self, handler):
self._handler = handler
self._queue = queue.Queue()
self._stop = threading.Event()
# Lifecycle closure and task cancellation are deliberately distinct.
# A graceful drain/close before the strict final translation gate must
# not poison the translation agent with E_TASK_CANCELLED.
self._cancel = threading.Event()
self._thread = threading.Thread(
target=self._run,
name="smartpaper-translation-worker",
daemon=True,
)
self._thread.start()
@property
def stop_event(self):
return self._stop
@property
def cancel_event(self):
return self._cancel
def submit(self, job: dict[str, Any]) -> Future:
future = Future()
self._queue.put((future, dict(job)))
return future
def _run(self):
while True:
item = self._queue.get()
try:
if item is None:
return
future, job = item
if not future.set_running_or_notify_cancel():
continue
try:
result = self._handler(job)
except BaseException as error:
future.set_exception(error)
else:
future.set_result(result)
finally:
self._queue.task_done()
def close(self, *, wait: bool = False) -> None:
if self._stop.is_set():
if wait and self._thread.is_alive():
self._thread.join(timeout=2.0)
return
# wait=True is the normal final-gate path after all queued work has
# been collected. Only an abortive/non-waiting shutdown signals the
# translation agent to cancel an in-flight model call.
if not wait:
self._cancel.set()
self._stop.set()
while True:
try:
item = self._queue.get_nowait()
except queue.Empty:
break
try:
if item is not None:
future, _job = item
future.cancel()
finally:
self._queue.task_done()
self._queue.put(None)
if wait and self._thread.is_alive():
self._thread.join(timeout=2.0)
class GenerationRunner:
def __init__(
self,
*,
task_dir: str | os.PathLike,
request_data: dict[str, Any],
execution_epoch: int,
executor_instance_id: str = "",
confirmed_checkpoint_seq: int | None = None,
confirmed_checkpoint_stage: str = "",
progress_callback: ProgressCallback | None = None,
cancel_checker: Callable[[], bool] | None = None,
):
self.task_dir = Path(task_dir)
self.request = dict(request_data)
self.execution_epoch = int(execution_epoch)
self.executor_instance_id = str(executor_instance_id or "")
self.confirmed_checkpoint_seq = (
None if confirmed_checkpoint_seq is None else max(0, int(confirmed_checkpoint_seq))
)
self.confirmed_checkpoint_stage = str(confirmed_checkpoint_stage or "")[:120]
self.progress_callback = progress_callback or (lambda **_: None)
self.cancel_checker = cancel_checker or (lambda: False)
expected_hash = str(self.request.get("request_hash") or "")
actual_hash = _canonical_request_hash(self.request)
if not expected_hash or expected_hash != actual_hash:
raise RuntimeError("request_hash_mismatch")
self.request_hash = expected_hash
self.checkpoints = TaskCheckpointStore(
self.task_dir,
self.request_hash,
self.execution_epoch,
task_id=str(self.request.get("task_id") or "") if self.executor_instance_id else "",
executor_instance_id=self.executor_instance_id,
confirmed_checkpoint_seq=self.confirmed_checkpoint_seq,
confirmed_checkpoint_stage=self.confirmed_checkpoint_stage,
)
self.output_dir = self.task_dir / "outputs"
self.output_dir.mkdir(parents=True, exist_ok=True)
self.agent = None
self.translation_agent = None
self._preview_lock = threading.RLock()
preview_token = str(self.request.get("preview_token") or "").strip()
self.preview_token = preview_token if re.fullmatch(r"[A-Za-z0-9_-]{32,128}", preview_token) else ""
self.preview_path = None
self._last_preview_write = 0.0
self._preview_english_text = ""
self._preview_english_stage = ""
self._preview_english_complete = False
self._preview_target_text = ""
self._preview_target_stage = "translation_waiting"
self._preview_target_complete = False
self._preview_target_english_commit_text = ""
self._translation_worker = None
self._translation_worker_sections = {}
self._translation_worker_glossary = {}
self._translation_worker_jobs = []
self._translation_worker_collected = 0
self._translation_worker_pending = set()
self._translation_worker_context = {}
# Display-only observability. These counters never decide task success,
# checkpoint authority, quota, or translation reuse.
self._translation_progress_sections = {}
self._translation_progress_queued = set()
self._translation_progress_inflight = set()
self._translation_progress_total = 0
self._qualitative_quote_sources = {}
# R14-TQ2-PQ1-R3-R1: evidence coverage is a quality signal, not a
# manuscript-completeness authority. It is persisted in checkpoints and
# exposed to the browser only as a stable status code.
self._preview_evidence_status = "normal"
self._evidence_coverage_events = []
if self.preview_token:
preview_dir = Path(__file__).resolve().parents[1] / "static" / "previews"
preview_dir.mkdir(parents=True, exist_ok=True)
self.preview_path = preview_dir / f"{self.preview_token}.json"
# Best-effort cleanup of stale display-only previews; task/checkpoint
# authority is unaffected by this cache.
cutoff = time.time() - 24 * 3600
for old_preview in preview_dir.glob("*.json"):
try:
if old_preview.stat().st_mtime < cutoff:
old_preview.unlink()
except OSError:
pass
def _compute_request_counts(self) -> tuple[int, int, int]:
"""Return UI metrics: DeepSeek requests and eligible literature results."""
agent = self.agent
if agent is None:
return 0, 0, 0
metrics = getattr(agent, "source_metrics", {}) or {}
s2_metric = metrics.get("s2", {}) if isinstance(metrics, dict) else {}
oa_metric = metrics.get("oa", {}) if isinstance(metrics, dict) else {}
translator_calls = int(getattr(getattr(self, "translation_agent", None), "ds_calls", 0) or 0)
return (
int(getattr(agent, "ds_calls", 0) or 0) + translator_calls,
int(s2_metric.get("eligible_result_count", 0) or 0),
int(oa_metric.get("eligible_result_count", 0) or 0),
)
def _progress(self, pct: int, node: str, *, checkpoint_seq=None, checkpoint_stage=None) -> None:
ds_calls, s2_calls, oa_calls = self._compute_request_counts()
self.progress_callback(
progress_pct=max(0, min(100, int(pct))),
current_node=str(node or "")[:240],
checkpoint_seq=checkpoint_seq,
checkpoint_stage=checkpoint_stage,
ds_calls=ds_calls,
s2_calls=s2_calls,
oa_calls=oa_calls,
)
def _checkpoint(self, stage: str, state: dict[str, Any], pct: int, *, display_node: str | None = None) -> None:
seq, normalized_stage = self.checkpoints.save(stage, state)
self._progress(
pct, display_node or normalized_stage,
checkpoint_seq=seq, checkpoint_stage=normalized_stage,
)
@staticmethod
def _preview_visible_text(value: str) -> str:
text = str(value or "")
# Never expose hidden reasoning. Raw CITE placeholders are also hidden
# from the user because they have not passed semantic citation QA yet;
# the authoritative manuscript buffer remains untouched.
text = re.sub(r"(?is)<think>.*?(?:</think>|$)", "", text)
text = re.sub(r"\s*\[?CITE-\d+\]?\s*", " ", text, flags=re.I)
text = re.sub(r"[ \t]{2,}", " ", text)
return text.strip()
@staticmethod
def _build_qualitative_quote_ownership(tasks, data_metadata):
"""Freeze one and only one Findings owner for every qualitative quote."""
evidence_by_tag = {
str(item.get("tag") or "").strip(): dict(item)
for item in (data_metadata or {}).get("evidence", [])
if isinstance(item, dict) and str(item.get("tag") or "").strip()
}
owners = {}
for task in tasks or []:
if str(task.get("section_id") or "") != "findings":
continue
owner = str(task.get("display_name") or "").strip()
for item in task.get("theme_evidence") or []:
if not isinstance(item, dict):
continue
tag = str(item.get("tag") or "").strip()
if not tag:
continue
if tag not in evidence_by_tag:
raise RuntimeError(f"Qualitative quotation ownership references unknown tag: {tag}")
if tag in owners:
raise RuntimeError(
f"Qualitative quotation has multiple Findings owners: {tag} -> {owners[tag]!r}, {owner!r}"
)
owners[tag] = owner
missing = sorted(set(evidence_by_tag) - set(owners))
if missing:
raise RuntimeError(f"Qualitative quotations lack a Findings owner: {missing[:8]}")
return owners
@staticmethod
def _qualitative_task_key(task):
task = task or {}
explicit = str(task.get("task_key") or "").strip()
if explicit:
return explicit
section_id = str(task.get("section_id") or task.get("section") or "section").strip()
stable_unit = str(task.get("theme_id") or task.get("subsection_id") or section_id).strip()
part_index = int(task.get("part_index") or 0) + 1
return f"{section_id}:{stable_unit}:part:{part_index:02d}"
@classmethod
def _build_qualitative_quote_invocation_plan(cls, tasks, data_metadata):
"""Freeze one deterministic body invocation for every retained quote.
Logical ownership is a stable Theme object. Runtime Part chunks only
select where the already-owned quote is rendered; they never become the
quote owner and can never change cardinality.
"""
metadata = data_metadata or {}
evidence_by_tag = {
str(item.get("tag") or "").strip(): dict(item)
for item in metadata.get("evidence", [])
if isinstance(item, dict) and str(item.get("tag") or "").strip()
}
theme_id_by_title = {}
for index, theme in enumerate(metadata.get("themes") or [], start=1):
if not isinstance(theme, dict):
continue
title = re.sub(r"\s+", " ", str(theme.get("title") or "")).strip().casefold()
if title:
theme_id_by_title[title] = str(theme.get("theme_id") or f"THEME-{index:02d}").strip()
plan = {}
task_slots = {}
for task in tasks or []:
if str(task.get("section_id") or "") != "findings":
continue
task_key = cls._qualitative_task_key(task)
task_theme_id = str(task.get("theme_id") or "").strip()
task_theme_title = str(task.get("theme_title") or task.get("subsection") or "").strip()
for item in task.get("theme_evidence") or []:
if not isinstance(item, dict):
continue
tag = str(item.get("tag") or "").strip()
if not tag:
continue
evidence = evidence_by_tag.get(tag)
if evidence is None:
raise RuntimeError(f"Qualitative quote plan references unknown evidence: {tag}")
if tag in plan:
raise RuntimeError(f"Qualitative quote plan has duplicate render assignment: {tag}")
owner_theme_id = str(
task_theme_id
or evidence.get("theme_id")
or theme_id_by_title.get(
re.sub(r"\s+", " ", str(evidence.get("theme") or task_theme_title)).strip().casefold()
)
or ""
).strip()
if not owner_theme_id:
raise RuntimeError(f"Qualitative quote lacks a stable theme owner: {tag}")
quote = str(evidence.get("quote") or "").strip()
if not quote:
raise RuntimeError(f"Qualitative quote is empty: {tag}")
quote_hash = str(evidence.get("quote_hash") or "").strip() or hashlib.sha256(
re.sub(r"\s+", " ", quote).strip().encode("utf-8")
).hexdigest()
slot_ordinal = task_slots.get(task_key, 0) + 1
task_slots[task_key] = slot_ordinal
plan[tag] = {
"quote_id": tag,
"quote_hash": quote_hash,
"owner_theme_id": owner_theme_id,
"owner_theme_title": str(evidence.get("theme") or task_theme_title).strip(),
"render_task_key": task_key,
"render_part_index": int(task.get("part_index") or 0),
"render_part_count": int(task.get("part_count") or 1),
"slot_ordinal": slot_ordinal,
"cardinality": 1,
}
missing = sorted(set(evidence_by_tag) - set(plan))
extra = sorted(set(plan) - set(evidence_by_tag))
if missing or extra:
raise RuntimeError(
f"Qualitative quote invocation plan mismatch: missing={missing[:8]} extra={extra[:8]}"
)
cls._validate_qualitative_quote_invocation_plan(plan, tasks, metadata)
return plan
@classmethod
def _validate_qualitative_quote_invocation_plan(cls, plan, tasks, data_metadata):
metadata = data_metadata or {}
evidence_by_tag = {
str(item.get("tag") or "").strip(): dict(item)
for item in metadata.get("evidence", [])
if isinstance(item, dict) and str(item.get("tag") or "").strip()
}
task_by_key = {cls._qualitative_task_key(task): task for task in tasks or []}
if set(plan or {}) != set(evidence_by_tag):
raise RuntimeError("Qualitative quote invocation plan does not match the frozen evidence ledger.")
for tag, record in (plan or {}).items():
if int(record.get("cardinality") or 0) != 1:
raise RuntimeError(f"Qualitative quote cardinality must equal one: {tag}")
owner_theme_id = str(record.get("owner_theme_id") or "").strip()
if not owner_theme_id or "part " in owner_theme_id.casefold():
raise RuntimeError(f"Qualitative quote owner is not a stable Theme object: {tag}")
task_key = str(record.get("render_task_key") or "").strip()
task = task_by_key.get(task_key)
if not task or str(task.get("section_id") or "") != "findings":
raise RuntimeError(f"Qualitative quote render task is invalid: {tag} -> {task_key!r}")
task_theme_id = str(task.get("theme_id") or "").strip()
if task_theme_id and task_theme_id != owner_theme_id:
raise RuntimeError(f"Qualitative quote render task crossed theme ownership: {tag}")
evidence = evidence_by_tag[tag]
quote = str(evidence.get("quote") or "").strip()
expected_hash = str(evidence.get("quote_hash") or "").strip() or hashlib.sha256(
re.sub(r"\s+", " ", quote).strip().encode("utf-8")
).hexdigest()
if str(record.get("quote_hash") or "") != expected_hash:
raise RuntimeError(f"Qualitative quote hash drifted after planning: {tag}")
return True
@staticmethod
def _strip_model_controlled_qualitative_quotes(text, data_metadata):
"""Remove model-emitted quote controls/literals before Executor rendering.
This is a containment boundary, not the quote control plane: quotation
presence and cardinality come exclusively from the frozen invocation
plan. The model's attempted tags/literals therefore have zero authority.
"""
value = str(text or "")
value = re.sub(r"(?<!\w)\[?QUOTE-\d+\]?(?!\w)", "", value, flags=re.I)
quotes = sorted(
{
str(item.get("quote") or "").strip()
for item in (data_metadata or {}).get("evidence", [])
if isinstance(item, dict) and str(item.get("quote") or "").strip()
},
key=len,
reverse=True,
)
for quote in quotes:
for wrapped in (f'"{quote}"', f'“{quote}”', f'«{quote}»', quote):
value = value.replace(wrapped, "")
value = re.sub(r"[ \t]+([,.;:!?])", r"\1", value)
value = re.sub(r"([,;:])\s*([.!?])", r"\2", value)
value = re.sub(r"[ \t]{2,}", " ", value)
value = re.sub(r"\n[ \t]+", "\n", value)
return value.strip()
@classmethod
def _render_planned_qualitative_quotes(cls, text, *, task, data_metadata, quote_invocation_plan):
"""Deterministically insert frozen quotations into one rendered task."""
value = cls._strip_model_controlled_qualitative_quotes(text, data_metadata)
task_key = cls._qualitative_task_key(task)
scheduled = [
dict(record)
for record in (quote_invocation_plan or {}).values()
if str(record.get("render_task_key") or "") == task_key
]
scheduled.sort(key=lambda record: (int(record.get("slot_ordinal") or 0), str(record.get("quote_id") or "")))
if not scheduled:
return value
evidence_by_tag = {
str(item.get("tag") or "").strip(): dict(item)
for item in (data_metadata or {}).get("evidence", [])
if isinstance(item, dict) and str(item.get("tag") or "").strip()
}
blocks = [block.strip() for block in re.split(r"\n\s*\n", value) if block.strip()]
anchor_indices = [
index for index, block in enumerate(blocks)
if not re.match(r"^(?:#{1,6}\s|\||[-*+]\s|>)", block)
]
if not anchor_indices:
anchor_indices = [len(blocks) - 1] if blocks else []
insertions = {}
for index, record in enumerate(scheduled):
tag = str(record.get("quote_id") or "").strip()
quote = str((evidence_by_tag.get(tag) or {}).get("quote") or "").strip()
if not quote:
raise RuntimeError(f"Planned qualitative quotation is unavailable at render time: {tag}")
if anchor_indices:
anchor_pos = min(
len(anchor_indices) - 1,
max(0, int(((index + 1) * len(anchor_indices)) / (len(scheduled) + 1))),
)
block_index = anchor_indices[anchor_pos]
insertions.setdefault(block_index, []).append(f'"{quote}"')
else:
blocks.append(f'"{quote}"')
rendered_blocks = []
for index, block in enumerate(blocks):
rendered_blocks.append(block)
rendered_blocks.extend(insertions.get(index, []))
rendered = "\n\n".join(rendered_blocks).strip()
for record in scheduled:
tag = str(record.get("quote_id") or "").strip()
quote = str((evidence_by_tag.get(tag) or {}).get("quote") or "").strip()
if rendered.count(quote) != 1:
raise RuntimeError(f"Qualitative deterministic quote rendering failed: {tag}")
return rendered
@staticmethod
def _validate_qualitative_quote_rendering(text, quote_invocation_plan, data_metadata):
body_text = re.split(
r"(?m)^#\s+Appendix(?:[:\s].*)?$", str(text or ""), maxsplit=1
)[0]
unresolved = sorted(set(re.findall(r"\bQUOTE-\d+\b", body_text, flags=re.I)))
if unresolved:
raise RuntimeError(f"Unresolved qualitative quote controls in body: {unresolved[:8]}")
normalized_body = re.sub(r"\s+", " ", body_text).casefold()
evidence_by_tag = {
str(item.get("tag") or "").strip(): dict(item)
for item in (data_metadata or {}).get("evidence", [])
if isinstance(item, dict) and str(item.get("tag") or "").strip()
}
for tag, record in (quote_invocation_plan or {}).items():
quote = re.sub(r"\s+", " ", str((evidence_by_tag.get(tag) or {}).get("quote") or "")).strip().casefold()
expected = int(record.get("cardinality") or 0)
actual = normalized_body.count(quote) if quote else 0
if actual != expected:
raise RuntimeError(
f"Qualitative quote invocation invariant failed: {tag} actual={actual} planned={expected}"
)
return True
@staticmethod
def _redact_qualitative_quotes(text, data_metadata):
"""Hide prior verbatim quote text from cross-unit continuity context."""
value = str(text or "")
quotes = sorted(
{
str(item.get("quote") or "").strip()
for item in (data_metadata or {}).get("evidence", [])
if isinstance(item, dict) and str(item.get("quote") or "").strip()
},
key=len,
reverse=True,
)
for quote in quotes:
value = value.replace(f'"{quote}"', "[prior qualitative quotation omitted]")
value = value.replace(quote, "[prior qualitative quotation omitted]")
return value
@classmethod
def _bind_owned_qualitative_quotes(
cls,
text,
*,
display_name,
data_metadata,
quote_ownership,
prior_manuscript="",
):
"""Bind only the current Findings unit's owned quotation tags.
Other manuscript units are not allowed to contain frozen quote text or
quote tags. A direct verbatim reproduction of an owned quote is
normalized back to its tag before deterministic binding, preserving the
frozen wording without letting model prose create a second copy.
"""
value = str(text or "")
evidence_by_tag = {
str(item.get("tag") or "").strip(): dict(item)
for item in (data_metadata or {}).get("evidence", [])
if isinstance(item, dict) and str(item.get("tag") or "").strip()
}
allowed = {
tag for tag, owner in (quote_ownership or {}).items()
if str(owner) == str(display_name)
}
raw_tags = set(re.findall(r"\bQUOTE-\d+\b", value))
unauthorized_tags = sorted(raw_tags - allowed)
if unauthorized_tags:
raise RuntimeError(
f"Qualitative quotation tag used outside owning Findings unit {display_name!r}: {unauthorized_tags[:8]}"
)
# Quote text itself is also fenced. Owned verbatim text is converted
# back to a tag and then bound once; unowned verbatim text fails early.
for tag, item in evidence_by_tag.items():
quote = str(item.get("quote") or "").strip()
if not quote or quote not in value:
continue
if tag not in allowed:
raise RuntimeError(
f"Qualitative quotation text used outside owning Findings unit: {tag} in {display_name!r}"
)
value = value.replace(f'"{quote}"', tag)
value = value.replace(quote, tag)
# A quote already present in frozen prior prose cannot be rebound later,
# even after resume. This makes ownership idempotent without new state.
normalized_prior = re.sub(r"\s+", " ", str(prior_manuscript or "")).casefold()
for tag in sorted(allowed):
quote = str((evidence_by_tag.get(tag) or {}).get("quote") or "").strip()
if quote and re.sub(r"\s+", " ", quote).casefold() in normalized_prior:
raise RuntimeError(f"Qualitative quotation already used before owning unit: {tag}")
for tag in sorted(allowed):
tag_pattern = rf"(?<!\w)\[?{re.escape(tag)}\]?(?!\w)"
count = len(re.findall(tag_pattern, value))
if count > 1:
raise RuntimeError(
f"Qualitative quotation repeated within owning Findings unit: {tag} count={count}"
)
if count == 1:
quote = str((evidence_by_tag.get(tag) or {}).get("quote") or "").strip()
value = re.sub(
tag_pattern,
lambda _match, frozen_quote=quote: f'"{frozen_quote}"',
value,
count=1,
)
# No unresolved quote tag may escape into citation QA or final output.
unresolved = sorted(set(re.findall(r"\bQUOTE-\d+\b", value)))
if unresolved:
raise RuntimeError(f"Unresolved qualitative quotation tags: {unresolved[:8]}")
return value
@staticmethod
def _qualitative_quote_source_map(data_metadata):
return {
str(item.get("tag") or "").strip(): str(item.get("quote") or "").strip()
for item in (data_metadata or {}).get("evidence", [])
if isinstance(item, dict)
and str(item.get("tag") or "").strip()
and str(item.get("quote") or "").strip()
}
@staticmethod
def _qualitative_quote_ledger_identity_ok(
record, *, target_lang: str, request_hash: str, source_quote_hashes: dict,
task_id: str = "", translation_contract_version: str = TRANSLATION_CONTRACT_VERSION,
) -> bool:
"""Type-safe identity gate for task-level target-language Quote reuse.
Missing scalar identity fields remain backward-compatible with pre-R4
checkpoints. Present fields must be strings and match exactly. Quote
hashes must be a mapping and match the current frozen English evidence.
execution_epoch is deliberately not part of logical identity so a newer
epoch can reuse the same QA-passed target rendering for the same task.
"""
if not isinstance(record, dict) or not isinstance(source_quote_hashes, dict):
return False
def scalar_matches(stored, current: str) -> bool:
if stored is None or stored == "":
return True
return isinstance(stored, str) and stored == str(current or "")
stored_hashes = record.get("source_quote_hashes")
if stored_hashes is None or stored_hashes == "":
hashes_match = True
else:
hashes_match = isinstance(stored_hashes, dict) and stored_hashes == source_quote_hashes
return (
scalar_matches(record.get("task_id"), task_id)
and scalar_matches(record.get("target_lang"), target_lang)
and scalar_matches(record.get("request_hash"), request_hash)
and scalar_matches(record.get("translation_contract_version"), translation_contract_version)
and hashes_match
)
@classmethod
def _prepare_qualitative_translation_source(cls, source, quote_sources, quote_ledger):
"""Replace every frozen qualitative quote with a typed semantic slot.
QUOTE identity is never inferred from translated narrative surface text.
Missing target objects are classified as pending and translated through
the dedicated typed-object path before the surrounding narrative runs.
"""
value = str(source or "")
pending = []
reused = []
for tag, quote in sorted((quote_sources or {}).items()):
wrapped = f'"{quote}"'
tagged = f'{wrapped} [{tag}]'
if tagged in value:
value = value.replace(tagged, f"[{tag}]")
(reused if tag in (quote_ledger or {}) else pending).append(tag)
continue
if wrapped not in value:
continue
value = value.replace(wrapped, f"[{tag}]")
(reused if tag in (quote_ledger or {}) else pending).append(tag)
return value, pending, reused
@classmethod
def _finalize_qualitative_translation_quotes(cls, translated, pending, reused, quote_ledger):
value = str(translated or "")
ledger = dict(quote_ledger or {})
ordered_tags = list(dict.fromkeys(list(pending or []) + list(reused or [])))
for tag in ordered_tags:
rendered_quote = str(ledger.get(tag) or "").strip()
if not rendered_quote:
raise RuntimeError(f"Qualitative quote translation ledger missing target text: {tag}")
pattern = rf"(?<!\w)\[?{re.escape(tag)}\]?(?!\w)"
count = len(re.findall(pattern, value))
if count != 1:
raise RuntimeError(
f"Qualitative quote deterministic target slot injection failed: {tag} matches={count}"
)
value = re.sub(pattern, lambda _m, q=rendered_quote: q, value, count=1)
unresolved = sorted(set(re.findall(r"\bQUOTE-\d+\b", value)))
if unresolved:
raise RuntimeError(f"Unresolved target-language qualitative quote tags: {unresolved[:8]}")
return value, ledger
def _translation_progress_snapshot(self) -> dict[str, int]:
"""Return non-authoritative logical target-unit progress for the UI."""
mapping = dict(getattr(self, '_translation_progress_sections', {}) or {})
context = getattr(self, '_translation_worker_context', {}) or {}
tasks = context.get('adaptive_structure') or [] if isinstance(context, dict) else []
logical_keys = []
if tasks:
logical_keys = ['__front_matter__'] + [
key for key, _title in self._unique_top_level_sections(tasks)
]
logical_keys = list(dict.fromkeys(key for key in logical_keys if key))
total = int(getattr(self, '_translation_progress_total', 0) or len(logical_keys))
if total <= 0 and logical_keys:
total = len(logical_keys)
qa_passed = 0
repair_pending = 0
inflight = set(getattr(self, '_translation_progress_inflight', set()) or set())
for key in logical_keys:
record = mapping.get(key) or {}
status = str(record.get('qa_status') or '')
if key not in inflight and status in {'passed', 'passed_with_warning'} and record.get('translated_text'):
qa_passed += 1
elif status == 'repair_pending':
repair_pending += 1
queued = len(set(getattr(self, '_translation_progress_queued', set()) or set()).intersection(logical_keys))
in_flight = len(inflight.intersection(logical_keys))
return {
'qa_passed': min(qa_passed, total) if total else qa_passed,
'repair_pending': min(repair_pending, total) if total else repair_pending,
'queued': min(queued, total) if total else queued,
'in_flight': min(in_flight, total) if total else in_flight,
'total': max(0, total),
}
def _write_preview_payload(self, *, force: bool = False) -> None:
with self._preview_lock:
if self.preview_path is None:
return
now = time.monotonic()
if not force and now - self._last_preview_write < 0.45:
return
payload = {
"task_id": str(self.request.get("task_id") or ""),
"request_hash": self.request_hash,
"execution_epoch": self.execution_epoch,
"stage": str(self._preview_english_stage or "")[:240],
"text": self._preview_visible_text(self._preview_english_text),
"complete": bool(self._preview_english_complete),
"target_stage": str(self._preview_target_stage or "")[:240],
"target_text": self._preview_visible_text(self._preview_target_text),
"target_complete": bool(self._preview_target_complete),
"target_english_commit_text": self._preview_visible_text(
self._preview_target_english_commit_text
),
"target_progress": self._translation_progress_snapshot(),
"evidence_status": str(self._preview_evidence_status or "normal")[:40],
"updated_at": datetime.now(timezone.utc).isoformat(),
}
temporary = self.preview_path.with_suffix(".json.tmp")
temporary.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
os.replace(temporary, self.preview_path)
self._last_preview_write = now
def _publish_preview(self, text: str, stage: str, *, force: bool = False, complete: bool = False) -> None:
with self._preview_lock:
self._preview_english_text = str(text or "")
self._preview_english_stage = str(stage or "")
self._preview_english_complete = bool(complete)
self._write_preview_payload(force=force)
def _publish_target_preview(
self, text: str, stage: str, *, force: bool = True, complete: bool = False,
english_commit_text: str | None = None,
) -> None:
with self._preview_lock:
self._preview_target_text = str(text or "")
self._preview_target_stage = str(stage or "")
self._preview_target_complete = bool(complete)
if english_commit_text is not None:
self._preview_target_english_commit_text = str(english_commit_text or "")
self._write_preview_payload(force=force)
def _stream_text(self, generator, *, preview_prefix: str | None = None, preview_stage: str = "") -> str:
chunks = []
for chunk in generator:
value = str(chunk or "")
if not value:
continue
chunks.append(value)
if preview_prefix is not None:
self._publish_preview(preview_prefix + "".join(chunks), preview_stage, force=False)
text = "".join(chunks)
if preview_prefix is not None:
self._publish_preview(preview_prefix + text, preview_stage, force=True)
return text
@staticmethod
def _text_hash(value: str) -> str:
normalized = re.sub(r'\s+', ' ', str(value or '')).strip()
return hashlib.sha256(normalized.encode('utf-8')).hexdigest()
@staticmethod
def _unique_top_level_sections(tasks):
output = []
seen = set()
for task in tasks or []:
key = str(task.get('section_id') or task.get('section') or '').strip()
title = str(task.get('section') or '').strip()
if key and title and key not in seen:
seen.add(key); output.append((key, title))
return output
@staticmethod
def _extract_top_level_section(manuscript: str, title: str) -> str:
value = str(manuscript or '')
match = re.search(
rf'(?ms)^#\s+{re.escape(str(title))}\s*$\n(.*?)(?=^#\s+[^#].*?$|\Z)', value
)
if not match:
return ''
return f"# {title}\n" + match.group(1).strip()
@staticmethod
def _english_prefix_through_top_level(manuscript: str, title: str) -> str:
value = str(manuscript or '')
match = re.search(
rf'(?ms)^#\s+{re.escape(str(title))}\s*$\n.*?(?=^#\s+[^#].*?$|\Z)', value
)
return value[:match.end()].strip() if match else value.strip()
@staticmethod
def _extract_front_matter(manuscript: str, first_section_title: str) -> str:
value = str(manuscript or '')
match = re.search(rf'(?m)^#\s+{re.escape(str(first_section_title))}\s*$', value)
return value[:match.start()].strip() if match else ''
@staticmethod
def _extract_body_from_first_section(manuscript: str, first_section_title: str) -> str:
value = str(manuscript or '')
match = re.search(rf'(?m)^#\s+{re.escape(str(first_section_title))}\s*$', value)
return value[match.start():].strip() if match else value.strip()
@staticmethod
def _repair_validate_study_fact_contract(
mode: str, agent, quant_engine, text: str, data_metadata: dict, *, section_name: str
):
value = str(text or '').strip()
diagnostics = []
if str(mode or '') == 'asq':
value, diagnostics = agent.repair_qualitative_study_fact_contract(
value, data_metadata, section_name=section_name
)
agent.validate_qualitative_study_fact_contract(
value, data_metadata, section_name=section_name
)
elif str(mode or '') == 'amj':
value, diagnostics = quant_engine.repair_quantitative_study_fact_contract(
value, data_metadata, section_name=section_name
)
quant_engine.validate_quantitative_study_fact_contract(
value, data_metadata, section_name=section_name
)
return value, list(diagnostics or [])
@staticmethod
def _section_integrity_hash(value: str) -> str:
exact = str(value or "").replace("\r\n", "\n").replace("\r", "\n").strip()
return hashlib.sha256(exact.encode("utf-8")).hexdigest()
@classmethod
def _build_section_integrity_seal(cls, section_key: str, section_title: str, section_text: str) -> dict:
return {
"version": ENGLISH_SECTION_SEAL_VERSION,
"section_key": str(section_key or ""),
"section_title": str(section_title or ""),
"sha256": cls._section_integrity_hash(section_text),
}
@staticmethod
def _replace_top_level_section(manuscript: str, title: str, replacement: str) -> str:
value = str(manuscript or "")
pattern = re.compile(
rf'(?ms)^#\s+{re.escape(str(title))}\s*$\n.*?(?=^#\s+[^#].*?$|\Z)'
)
matches = list(pattern.finditer(value))
if len(matches) != 1:
raise RuntimeError(f"top_level_section_replace_count_invalid:{title}:{len(matches)}")
match = matches[0]
rendered = str(replacement or "").strip()
return (value[:match.start()] + rendered + "\n\n" + value[match.end():].lstrip("\n")).strip() + "\n\n"
@classmethod
def _validate_section_integrity_seal(cls, seal: dict, section_text: str, section_title: str) -> None:
record = dict(seal or {})
if record.get("version") != ENGLISH_SECTION_SEAL_VERSION:
raise RuntimeError(f"english_section_integrity_seal_version_invalid:{section_title}")
expected = str(record.get("sha256") or "")
actual = cls._section_integrity_hash(section_text)
if not expected or expected != actual:
raise RuntimeError(f"english_section_integrity_seal_mismatch:{section_title}")
@staticmethod
def _replace_keywords(manuscript: str, keywords) -> str:
value = str(manuscript or '')
if isinstance(keywords, (list, tuple)):
rendered = ', '.join(str(item).strip() for item in keywords if str(item).strip())
else:
rendered = str(keywords or '').strip()
pattern = re.compile(r'(?ms)(^##\s+Keywords\s*$\n)(.*?)(?=^#\s+[^#].*?$|\Z)')
matches = list(pattern.finditer(value))
if len(matches) != 1:
raise RuntimeError(f'keywords_heading_count_invalid:{len(matches)}')
match = matches[0]
return value[:match.start()] + match.group(1) + rendered + '\n\n' + value[match.end():].lstrip('\n')
@staticmethod
def _section_is_complete(tasks, completed_display_names, section_key: str) -> bool:
relevant = [
str(task.get('display_name') or '') for task in (tasks or [])
if str(task.get('section_id') or task.get('section') or '') == str(section_key)
]
done = set(str(item) for item in (completed_display_names or []))
return bool(relevant) and all(name in done for name in relevant)
@staticmethod
def _glossary_version(glossary) -> str:
encoded = json.dumps(dict(glossary or {}), ensure_ascii=False, sort_keys=True).encode('utf-8')
return hashlib.sha256(encoded).hexdigest()[:16]
@staticmethod
def _build_canonical_concept_ledger(
mode: str, data_metadata: dict, *, keywords='', adaptive_structure=None, research_logic_contract=''
) -> dict:
"""Freeze task-level research concept identity before body translation.
This ledger is intentionally conservative: quantitative construct names
come directly from frozen metadata; qualitative concept/theme labels come
from the Evidence Ledger/outline. Lower-case hyphenated concepts used in
the frozen research logic are added because the legacy Title-Case glossary
discovery cannot see them. No semantic synonym merging is performed.
"""
items = []
index_by_key = {}
def add(value, kind, policy='FLEXIBLE_TERM'):
term = re.sub(r'\s+', ' ', str(value or '')).strip(' \t\r\n,;:.')
if not term or len(term) < 3 or len(term) > 100:
return
words = re.findall(r"[A-Za-zÀ-ÖØ-öø-ÿА-Яа-яЁёӘәҒғҚқҢңӨөҰұҮүҺһІі][\w’'\-]*", term, flags=re.UNICODE)
if not words or len(words) > 10:
return
key = re.sub(r'[–—-]+', '-', term).casefold()
normalized_policy = 'STRICT_CONSTRUCT' if str(policy or '').upper() == 'STRICT_CONSTRUCT' else 'FLEXIBLE_TERM'
if key in index_by_key:
row = items[index_by_key[key]]
if normalized_policy == 'STRICT_CONSTRUCT' and row.get('policy') != 'STRICT_CONSTRUCT':
row['policy'] = 'STRICT_CONSTRUCT'
row['kind'] = str(kind or row.get('kind') or 'concept')
return
index_by_key[key] = len(items)
items.append({'source': term, 'kind': str(kind or 'concept'), 'policy': normalized_policy})
meta = dict(data_metadata or {})
if str(mode or '') == 'amj':
constructs = meta.get('constructs') if isinstance(meta.get('constructs'), dict) else {}
for role in ('predictor', 'mediator', 'outcome'):
add(constructs.get(role), f'quantitative_{role}', 'STRICT_CONSTRUCT')
else:
for theme in (meta.get('themes') or []):
if not isinstance(theme, dict):
continue
add(theme.get('title'), 'qualitative_theme', 'STRICT_CONSTRUCT')
for concept in (theme.get('concepts') or []):
add(concept, 'qualitative_concept', 'STRICT_CONSTRUCT')
for row in (meta.get('evidence') or []):
if isinstance(row, dict):
add(row.get('concept'), 'qualitative_concept', 'STRICT_CONSTRUCT')
for task in (adaptive_structure or []):
if not isinstance(task, dict):
continue
for concept in (task.get('concepts') or []):
add(concept, 'outline_concept', 'FLEXIBLE_TERM')
# User keywords remain planning seeds, not canonical identities. Use
# them only as a fallback when frozen empirical/qualitative metadata and
# outline concepts provide no concept labels at all.
if not items:
for seed in re.split(r'[,;\n|]+', str(keywords or '')):
seed = re.sub(r'\s+', ' ', seed).strip()
if 1 <= len(seed.split()) <= 6:
add(seed, 'keyword_fallback', 'FLEXIBLE_TERM')
# Capture explicit lower-case hyphenated academic expressions only when
# they recur or carry a research-concept cue. This closes the
# context-awareness/context-aware blind spot without freezing generic
# methodological adjectives such as cross-sectional by default.
hyphenated = re.findall(r'(?<![A-Za-z])(?:[a-z][a-z]{2,}(?:-[a-z][a-z]{2,})+)(?![A-Za-z])', str(research_logic_contract or ''))
concept_cues = {
'awareness','trust','governance','literacy','fidelity','performance','context','capability',
'calibration','memory','acceptance','effectiveness','efficiency','alignment','recommendation',
}
counts = {}
for term in hyphenated:
counts[term.casefold()] = counts.get(term.casefold(), 0) + 1
for term in hyphenated:
parts = set(term.casefold().split('-'))
if counts.get(term.casefold(), 0) >= 2 or parts.intersection(concept_cues):
add(term, 'logic_hyphenated_concept', 'FLEXIBLE_TERM')
return {'version': 'concept-ledger-v2', 'items': items}
@staticmethod
def _canonical_concept_writer_directive(ledger: dict) -> str:
terms = [str(item.get('source') or '').strip() for item in (dict(ledger or {}).get('items') or []) if isinstance(item, dict)]
terms = [term for term in terms if term]
if not terms:
return ''
rendered = '; '.join(terms[:40])
return (
'CANONICAL CONCEPT IDENTITY: The following research labels are frozen task-level concepts: '
+ rendered
+ '. When referring to the same construct or concept, preserve its canonical English label; '
'do not silently replace it with a near-synonym that changes concept identity. Ordinary grammatical '
'articles, possessives, and surrounding syntax remain free. Distinct listed labels are not synonyms unless the evidence explicitly says so.'
)
@staticmethod
def _translation_ledger_hash(agent, source: str) -> str:
method = getattr(agent, 'translation_fact_ledger_hash', None)
if callable(method):
return str(method(source) or '')
# Test doubles / legacy adapters still get deterministic invalidation.
return hashlib.sha256(str(source or '').encode('utf-8')).hexdigest()
def _translation_identity_bundle(self, agent, source: str) -> dict[str, str]:
"""Return one canonical identity domain for translation reuse.
The frozen English source remains the manuscript authority, while the
Translation Compiler may consume a canonicalized representation (for
example qualitative quote literals become QUOTE semantic slots). Cache
identity must therefore persist both hashes and derive the private Fact
Ledger only from the exact compiled source sent to the translator.
"""
raw_source = str(source or '').strip()
compiled_source, _pending, _reused = self._prepare_qualitative_translation_source(
raw_source, getattr(self, '_qualitative_quote_sources', {}), {}
)
compiled_source = str(compiled_source or '').strip()
return {
'source_hash': self._text_hash(raw_source),
'compiled_source': compiled_source,
'compiled_source_hash': self._text_hash(compiled_source),
'fact_ledger_hash': self._translation_ledger_hash(agent, compiled_source),
}
@staticmethod
def _assemble_partial_translation(translation_sections, adaptive_structure, *, include_front=False) -> str:
mapping = translation_sections if isinstance(translation_sections, dict) else {}
parts = []
if include_front:
record = mapping.get('__front_matter__') or {}
if record.get('qa_status') in {'passed', 'passed_with_warning'} and record.get('translated_text'):
parts.append(str(record['translated_text']).strip())
for key, _title in GenerationRunner._unique_top_level_sections(adaptive_structure):
record = mapping.get(key) or {}
if record.get('qa_status') in {'passed', 'passed_with_warning'} and record.get('translated_text'):
parts.append(str(record['translated_text']).strip())
return '\n\n'.join(part for part in parts if part).strip()
def _start_translation_worker(
self, agent, *, translation_sections, translation_glossary,
adaptive_structure, target_lang,
) -> None:
if self._translation_worker is not None:
return
self.translation_agent = agent
self._translation_worker_sections = dict(translation_sections or {})
self._translation_worker_glossary = dict(translation_glossary or {})
self._translation_worker_context = {
'adaptive_structure': list(adaptive_structure or []),
'target_lang': str(target_lang or ''),
}
logical_keys = ['__front_matter__'] + [
key for key, _title in self._unique_top_level_sections(adaptive_structure)
]
logical_keys = list(dict.fromkeys(key for key in logical_keys if key))
self._translation_progress_total = len(logical_keys)
self._translation_progress_sections = dict(translation_sections or {})
self._translation_progress_queued = {
key for key in logical_keys if key in self._translation_progress_sections
}
self._translation_progress_inflight = set()
self._translation_worker = _OrderedTranslationWorker(self._run_translation_job)
self._write_preview_payload(force=True)
def _run_translation_job(self, job: dict[str, Any]) -> dict[str, Any]:
if self.translation_agent is None:
raise RuntimeError('translation_worker_agent_missing')
context = dict(self._translation_worker_context or {})
sections, glossary = self._translate_frozen_unit(
self.translation_agent,
unit_key=str(job.get('unit_key') or ''),
unit_name=str(job.get('unit_name') or ''),
source_text=str(job.get('source_text') or ''),
target_lang=str(context.get('target_lang') or ''),
translation_sections=self._translation_worker_sections,
translation_glossary=self._translation_worker_glossary,
adaptive_structure=context.get('adaptive_structure') or [],
include_front=True,
defer_failures=True,
english_commit_text=str(job.get('english_commit_text') or ''),
)
self._translation_worker_sections = dict(sections or {})
self._translation_worker_glossary = dict(glossary or {})
return {
'unit_key': str(job.get('unit_key') or ''),
'source_hash': str(job.get('source_hash') or ''),
'translation_sections': dict(self._translation_worker_sections),
'translation_glossary': dict(self._translation_worker_glossary),
}
def _enqueue_translation_unit(
self, agent, *, unit_key: str, unit_name: str, source_text: str,
translation_sections: dict, english_commit_text: str,
) -> bool:
source = str(source_text or '').strip()
if not source or self._translation_worker is None:
return False
identity = self._translation_identity_bundle(agent, source)
source_hash = identity['source_hash']
compiled_source_hash = identity['compiled_source_hash']
fact_ledger_hash = identity['fact_ledger_hash']
entity_ledger_method = getattr(agent, 'translation_protected_entity_ledger_hash', None)
protected_entity_ledger_hash = str(
entity_ledger_method(identity['compiled_source'], self._translation_worker_glossary)
if callable(entity_ledger_method) else ''
)
protected_entity_ledger_version = str(
getattr(agent, 'PROTECTED_ENTITY_LEDGER_VERSION', 'protected-entity-ledger-v1')
or 'protected-entity-ledger-v1'
)
existing = dict((translation_sections or {}).get(unit_key) or {})
if not isinstance(getattr(self, '_translation_progress_queued', None), set):
self._translation_progress_queued = set()
if not isinstance(getattr(self, '_translation_progress_inflight', None), set):
self._translation_progress_inflight = set()
# Do not replace the worker-owned display snapshot with a possibly stale
# main-thread copy here; collection or the worker completion path updates
# _translation_progress_sections monotonically.
self._translation_progress_queued.add(str(unit_key))
if (
existing.get('qa_status') in {'passed', 'passed_with_warning'}
and existing.get('source_hash') == source_hash
and existing.get('compiled_source_hash') == compiled_source_hash
and existing.get('target_lang') == str(self._translation_worker_context.get('target_lang') or '')
and existing.get('translation_contract_version') == TRANSLATION_CONTRACT_VERSION
and existing.get('fact_ledger_hash') == fact_ledger_hash
and existing.get('protected_entity_ledger_version') == protected_entity_ledger_version
and existing.get('protected_entity_ledger_hash') == protected_entity_ledger_hash
and existing.get('translated_text')
):
return False
if existing and existing.get('translated_text'):
reasons = []
if existing.get('source_hash') != source_hash: reasons.append('english_source_hash')
if existing.get('compiled_source_hash') != compiled_source_hash: reasons.append('compiled_source_hash')
if existing.get('fact_ledger_hash') != fact_ledger_hash: reasons.append('fact_ledger_hash')
if existing.get('protected_entity_ledger_version') != protected_entity_ledger_version: reasons.append('protected_entity_ledger_version')
if existing.get('protected_entity_ledger_hash') != protected_entity_ledger_hash: reasons.append('protected_entity_ledger_hash')
if existing.get('translation_contract_version') != TRANSLATION_CONTRACT_VERSION: reasons.append('contract_version')
if existing.get('target_lang') != str(self._translation_worker_context.get('target_lang') or ''): reasons.append('target_lang')
if existing.get('qa_status') not in {'passed', 'passed_with_warning'}: reasons.append('qa_status')
logging.info('Translation reuse miss | unit=%s phase=enqueue reasons=%s', unit_name, ','.join(reasons) or 'unknown')
pending_key = (str(unit_key), source_hash, compiled_source_hash, fact_ledger_hash, protected_entity_ledger_version, protected_entity_ledger_hash)
if pending_key in self._translation_worker_pending:
return False
self._translation_progress_inflight.add(str(unit_key))
future = self._translation_worker.submit({
'unit_key': str(unit_key),
'unit_name': str(unit_name),
'source_text': source,
'source_hash': source_hash,
'compiled_source_hash': compiled_source_hash,
'fact_ledger_hash': fact_ledger_hash,
'english_commit_text': str(english_commit_text or ''),
})
self._translation_worker_pending.add(pending_key)
self._translation_worker_jobs.append({
'pending_key': pending_key,
'future': future,
})
self._write_preview_payload(force=True)
return True
def _collect_translation_results(self, translation_sections, translation_glossary, *, block=False):
sections = dict(translation_sections or {})
glossary = dict(translation_glossary or {})
while self._translation_worker_collected < len(self._translation_worker_jobs):
item = self._translation_worker_jobs[self._translation_worker_collected]
future = item['future']
if not block and not future.done():
break
result = future.result()
sections = dict(result.get('translation_sections') or sections)
glossary = dict(result.get('translation_glossary') or glossary)
self._translation_progress_sections = dict(sections)
self._translation_worker_pending.discard(item['pending_key'])
self._translation_worker_collected += 1
return sections, glossary
def _shutdown_translation_worker(self, *, wait=False) -> None:
worker = self._translation_worker
if worker is None:
return
worker.close(wait=bool(wait))
self._translation_worker = None
def _translate_frozen_unit(
self, agent, *, unit_key: str, unit_name: str, source_text: str, target_lang: str,
translation_sections: dict, translation_glossary: dict, adaptive_structure, include_front=False,
defer_failures=True, english_commit_text: str | None = None, final_gate: bool = False,
):
source = str(source_text or '').strip()
if not source:
return translation_sections, translation_glossary
identity = self._translation_identity_bundle(agent, source)
source_hash = identity['source_hash']
compiled_source_hash = identity['compiled_source_hash']
canonical_translation_source = identity['compiled_source']
contract_version = TRANSLATION_CONTRACT_VERSION
fact_ledger_hash = identity['fact_ledger_hash']
entity_ledger_method = getattr(agent, 'translation_protected_entity_ledger_hash', None)
protected_entity_ledger_hash = str(
entity_ledger_method(canonical_translation_source, translation_glossary)
if callable(entity_ledger_method) else ''
)
protected_entity_ledger_version = str(
getattr(agent, 'PROTECTED_ENTITY_LEDGER_VERSION', 'protected-entity-ledger-v1') or 'protected-entity-ledger-v1'
)
existing = dict((translation_sections or {}).get(unit_key) or {})
if not isinstance(getattr(self, '_translation_progress_queued', None), set):
self._translation_progress_queued = set()
if not isinstance(getattr(self, '_translation_progress_inflight', None), set):
self._translation_progress_inflight = set()
self._translation_progress_sections = dict(translation_sections or {})
self._translation_progress_queued.add(str(unit_key))
if (
existing.get('qa_status') in {'passed', 'passed_with_warning'}
and existing.get('source_hash') == source_hash
and existing.get('compiled_source_hash') == compiled_source_hash
and existing.get('target_lang') == target_lang
and existing.get('translation_contract_version') == contract_version
and existing.get('fact_ledger_hash') == fact_ledger_hash
and existing.get('protected_entity_ledger_version') == protected_entity_ledger_version
and existing.get('protected_entity_ledger_hash') == protected_entity_ledger_hash
and existing.get('translated_text')
):
self._translation_progress_inflight.discard(str(unit_key))
return translation_sections, translation_glossary
if existing and existing.get('translated_text'):
reasons = []
if existing.get('source_hash') != source_hash: reasons.append('english_source_hash')
if existing.get('compiled_source_hash') != compiled_source_hash: reasons.append('compiled_source_hash')
if existing.get('fact_ledger_hash') != fact_ledger_hash: reasons.append('fact_ledger_hash')
if existing.get('protected_entity_ledger_version') != protected_entity_ledger_version: reasons.append('protected_entity_ledger_version')
if existing.get('protected_entity_ledger_hash') != protected_entity_ledger_hash: reasons.append('protected_entity_ledger_hash')
if existing.get('translation_contract_version') != contract_version: reasons.append('contract_version')
if existing.get('target_lang') != target_lang: reasons.append('target_lang')
if existing.get('qa_status') not in {'passed', 'passed_with_warning'}: reasons.append('qa_status')
logging.info('Translation reuse miss | unit=%s phase=final_or_direct reasons=%s', unit_name, ','.join(reasons) or 'unknown')
self._translation_progress_inflight.add(str(unit_key))
partial = self._assemble_partial_translation(translation_sections, adaptive_structure, include_front=include_front)
initial_target_stage = f'translation_final_gate:{unit_name}' if final_gate else f'translating:{unit_name}'
self._publish_target_preview(
partial, initial_target_stage, force=True, complete=False,
english_commit_text=english_commit_text,
)
updated_sections = dict(translation_sections or {})
raw_quote_ledger_record = updated_sections.get("__quote_ledger__")
if raw_quote_ledger_record is not None and not isinstance(raw_quote_ledger_record, dict):
logging.warning(
"Qualitative quote translation ledger ignored malformed record type=%s",
type(raw_quote_ledger_record).__name__,
)
quote_ledger_record = (
dict(raw_quote_ledger_record) if isinstance(raw_quote_ledger_record, dict) else {}
)
current_quote_hashes = {
tag: hashlib.sha256(re.sub(r"\s+", " ", quote).strip().encode("utf-8")).hexdigest()
for tag, quote in getattr(self, "_qualitative_quote_sources", {}).items()
}
current_request_hash = str(getattr(self, "request_hash", "") or "")
current_task_id = str(getattr(getattr(self, "task_dir", None), "name", "") or "")
ledger_identity_ok = self._qualitative_quote_ledger_identity_ok(
quote_ledger_record,
task_id=current_task_id,
target_lang=target_lang,
request_hash=current_request_hash,
translation_contract_version=TRANSLATION_CONTRACT_VERSION,
source_quote_hashes=current_quote_hashes,
)
stored_translations = quote_ledger_record.get("translations")
quote_ledger = (
dict(stored_translations)
if ledger_identity_ok and isinstance(stored_translations, dict)
else {}
)
translation_source, pending_quote_tags, reused_quote_tags = self._prepare_qualitative_translation_source(
source, getattr(self, "_qualitative_quote_sources", {}), quote_ledger
)
translation_source = str(translation_source or '').strip()
if translation_source != canonical_translation_source:
raise RuntimeError('Canonical translation source changed with Quote Ledger state.')
def publish_translation_stage(stage):
value = str(stage or '').strip().casefold()
if value == 'qa':
preview_stage = f'translation_qa:{unit_name}'
elif value == 'repairing':
preview_stage = f'translation_repairing:{unit_name}'
elif value == 'adjudicating':
preview_stage = f'translation_adjudicating:{unit_name}'
elif final_gate:
preview_stage = f'translation_final_gate:{unit_name}'
else:
preview_stage = f'translating:{unit_name}'
self._publish_target_preview(
partial, preview_stage, force=True, complete=False,
english_commit_text=english_commit_text,
)
try:
if pending_quote_tags:
quote_translate_method = getattr(agent, 'translate_qualitative_quote_objects', None)
if not callable(quote_translate_method):
raise RuntimeError('Typed qualitative quote translation entry point is unavailable.')
quote_objects = {
tag: getattr(self, '_qualitative_quote_sources', {}).get(tag, '')
for tag in pending_quote_tags
}
quote_result = quote_translate_method(
quote_objects,
target_lang,
glossary=translation_glossary,
max_attempts=2,
stage_callback=publish_translation_stage,
)
returned_quotes = (quote_result or {}).get('translations')
if not isinstance(returned_quotes, dict) or set(returned_quotes) != set(pending_quote_tags):
raise RuntimeError(
'Typed qualitative quote translation identity mismatch: '
f'expected={sorted(pending_quote_tags)} got={sorted(returned_quotes) if isinstance(returned_quotes, dict) else []}'
)
for tag in pending_quote_tags:
rendered_quote = str(returned_quotes.get(tag) or '').strip()
if not rendered_quote:
raise RuntimeError(f'Typed qualitative quote translation is empty: {tag}')
quote_ledger[tag] = rendered_quote
returned_glossary = (quote_result or {}).get('glossary')
if isinstance(returned_glossary, dict):
translation_glossary = dict(returned_glossary)
updated_sections['__quote_ledger__'] = {
'task_id': getattr(getattr(self, "task_dir", None), "name", ""),
'request_hash': current_request_hash,
'writer_execution_epoch': int(getattr(self, "execution_epoch", 0) or 0),
'target_lang': target_lang,
'translation_contract_version': TRANSLATION_CONTRACT_VERSION,
'source_quote_hashes': current_quote_hashes,
'translations': dict(quote_ledger),
'updated_at': datetime.now(timezone.utc).isoformat(),
}
translate_method = agent.translate_frozen_section
call_kwargs = {
'glossary': translation_glossary, 'section_name': unit_name, 'max_attempts': 2,
}
try:
parameters = list(inspect.signature(translate_method).parameters.values())
supports_stage_callback = any(
item.name == 'stage_callback' or item.kind == inspect.Parameter.VAR_KEYWORD
for item in parameters
)
supports_claim_states = any(
item.name == 'claim_states' or item.kind == inspect.Parameter.VAR_KEYWORD
for item in parameters
)
except (TypeError, ValueError):
supports_stage_callback = False
supports_claim_states = False
if supports_stage_callback:
call_kwargs['stage_callback'] = publish_translation_stage
if supports_claim_states and isinstance(existing.get('claim_states'), dict):
call_kwargs['claim_states'] = dict(existing.get('claim_states') or {})
result = translate_method(translation_source, target_lang, **call_kwargs)
translated_text = str(result.get('text') or '').strip()
if pending_quote_tags or reused_quote_tags:
translated_text, quote_ledger = self._finalize_qualitative_translation_quotes(
translated_text, pending_quote_tags, reused_quote_tags, quote_ledger
)
agent.validate_translation_alignment(source, translated_text)
result = dict(result)
result['text'] = translated_text
updated_sections['__quote_ledger__'] = {
'task_id': getattr(getattr(self, "task_dir", None), "name", ""),
'request_hash': current_request_hash,
'writer_execution_epoch': int(getattr(self, "execution_epoch", 0) or 0),
'target_lang': target_lang,
'translation_contract_version': TRANSLATION_CONTRACT_VERSION,
'source_quote_hashes': current_quote_hashes,
'translations': dict(quote_ledger),
'updated_at': datetime.now(timezone.utc).isoformat(),
}
except PipelineCancelledError:
raise
except Exception as error:
# Persist repair state for both intermediate and strict final gates.
# The caller owns checkpoint authority and decides whether to stop.
code_method = getattr(agent, 'translation_error_code', None)
error_code = str(code_method(error) if callable(code_method) else 'TRANSLATION_PENDING')
updated_sections[unit_key] = {
'source_hash': source_hash,
'compiled_source_hash': compiled_source_hash,
'target_lang': target_lang,
'glossary_version': self._glossary_version(translation_glossary),
'translation_contract_version': contract_version,
'fact_ledger_hash': fact_ledger_hash,
'protected_entity_ledger_version': protected_entity_ledger_version,
'protected_entity_ledger_hash': protected_entity_ledger_hash,
'translated_text': '',
'qa_status': 'repair_pending',
'claim_states': dict(getattr(agent, '_last_translation_claim_states', {}) or {}),
'claim_state_version': 'translation-claim-state-v2',
'error_code': error_code,
'attempts': int(existing.get('attempts') or 0) + 1,
'deferred_at': datetime.now(timezone.utc).isoformat(),
}
logging.warning(
'Translation repair pending | unit=%s final_gate=%s code=%s error=%s',
unit_name, bool(final_gate), error_code, type(error).__name__,
)
self._translation_progress_sections = dict(updated_sections)
self._translation_progress_inflight.discard(str(unit_key))
self._publish_target_preview(
partial, f'translation_repair_pending:{unit_name}', force=True, complete=False,
english_commit_text=english_commit_text,
)
return updated_sections, translation_glossary
updated_glossary = dict(result.get('glossary') or translation_glossary or {})
result_fact_ledger_hash = str(result.get('fact_ledger_hash') or fact_ledger_hash)
if result_fact_ledger_hash != fact_ledger_hash:
raise RuntimeError('Translation result Fact Ledger identity differs from canonical compiled source.')
updated_sections[unit_key] = {
'source_hash': source_hash,
'compiled_source_hash': compiled_source_hash,
'target_lang': target_lang,
'glossary_version': self._glossary_version(updated_glossary),
'translation_contract_version': str(result.get('translation_contract_version') or contract_version),
'fact_ledger_hash': result_fact_ledger_hash,
'protected_entity_ledger_version': str(result.get('protected_entity_ledger_version') or protected_entity_ledger_version),
'protected_entity_ledger_hash': str(result.get('protected_entity_ledger_hash') or protected_entity_ledger_hash),
'translated_text': str(result.get('text') or '').strip(),
'qa_status': str(result.get('qa_status') or 'passed'),
'language_adapter': str(result.get('language_adapter') or ''),
'diagnostics': list(result.get('diagnostics') or [])[-20:],
'claim_states': dict(result.get('claim_states') or {}),
'claim_state_version': str(result.get('claim_state_version') or 'translation-claim-state-v2'),
'attempts': int(result.get('attempts') or 1),
'completed_at': datetime.now(timezone.utc).isoformat(),
}
partial = self._assemble_partial_translation(updated_sections, adaptive_structure, include_front=include_front)
self._translation_progress_sections = dict(updated_sections)
self._translation_progress_inflight.discard(str(unit_key))
self._publish_target_preview(
partial, f'translation_ready:{unit_name}', force=True, complete=False,
english_commit_text=english_commit_text,
)
return updated_sections, updated_glossary
@staticmethod
def _bounded_target(base_target: int, accumulated_deviation: float, remaining_including_current: int, safe_chunk: int) -> int:
base = max(80, int(base_target))
correction = -float(accumulated_deviation) / max(1, int(remaining_including_current))
lower = max(80, int(round(base * 0.75)))
upper = min(int(safe_chunk * 1.5), max(lower, int(round(base * 1.25))))
return max(lower, min(upper, int(round(base + correction))))
@staticmethod
def _english_count_for_target(manuscript: str) -> int:
body = re.split(r"(?m)^#\s+References\s*$", str(manuscript or ""), maxsplit=1)[0]
body = re.split(r"(?m)^#\s+Appendix: Data Structure and Evidence\s*$", body, maxsplit=1)[0]
# Markdown punctuation is ignored; contractions/hyphenated terms stay one token.
return len(re.findall(r"\b[A-Za-z0-9]+(?:[-'][A-Za-z0-9]+)*\b", body))
def _validate_target_length(self, manuscript: str, target_words: int) -> dict[str, Any]:
"""Return a nonfatal total-length quality diagnostic.
The user-selected target remains a planning input, not a completion gate.
Completeness is validated separately by structure, evidence, citation,
translation, DOCX, and ZIP checks.
"""
quality = APP_CONFIG.get("manuscript_validation", {})
minimum = int(target_words * float(quality.get("minimum_total_ratio", 0.97)))
maximum = int(target_words * float(quality.get("maximum_total_ratio", 1.03)))
short_notice = float(quality.get("total_short_notice_ratio", 0.70))
topic_review = float(quality.get("total_topic_review_ratio", 0.50))
actual = self._english_count_for_target(manuscript)
ratio = (actual / max(1, int(target_words)))
status = "normal"
recommendation = ""
if ratio < topic_review:
status = "very_short"
recommendation = (
"First verify that generation completed normally and no section/evidence is missing. "
"If the manuscript is complete, consider broadening the research scope or selecting a new research topic."
)
elif ratio < short_notice:
status = "short"
recommendation = (
"If the manuscript is structurally complete, consider broadening the theoretical scope, context, "
"research question, or research topic."
)
elif actual < minimum:
status = "slightly_short"
elif actual > maximum:
status = "long"
if status != "normal":
logging.warning(
"Manuscript length diagnostic | actual=%s | target=%s | ratio=%.3f | status=%s | nonfatal=true",
actual, target_words, ratio, status,
)
return {
"scope": "manuscript", "name": "English manuscript", "actual_words": actual,
"target_words": int(target_words), "ratio": round(ratio, 4), "status": status,
"recommendation": recommendation, "nonfatal": True,
}
@staticmethod
def _upsert_quality_diagnostic(items: list[dict[str, Any]], diagnostic: dict[str, Any]) -> None:
key = (str(diagnostic.get("scope") or ""), str(diagnostic.get("name") or ""))
items[:] = [
item for item in items
if (str(item.get("scope") or ""), str(item.get("name") or "")) != key
]
items.append(dict(diagnostic))
def _section_word_diagnostic(self, name: str, actual_words: int, target_words: int) -> dict[str, Any]:
validation = APP_CONFIG.get("manuscript_validation", {})
budget = APP_CONFIG.get("word_budget", {})
target = max(1, int(target_words))
actual = max(0, int(actual_words))
ratio = actual / target
short_notice = float(validation.get("section_short_notice_ratio", 0.60))
topic_review = float(validation.get("section_topic_review_ratio", 0.40))
preferred_min = float(budget.get("section_min_ratio", 0.92))
preferred_max = float(budget.get("section_max_ratio", 1.08))
status = "normal"
recommendation = ""
if ratio < topic_review:
status = "very_short"
recommendation = (
"First verify that this section completed normally and that evidence was available. "
"If it is complete, consider broadening the research scope or selecting a new research topic."
)
elif ratio < short_notice:
status = "short"
recommendation = (
"If the section is complete, consider broadening the theoretical scope, context, research question, "
"or research topic."
)
elif ratio < preferred_min:
status = "slightly_short"
elif ratio > preferred_max:
status = "long"
if status != "normal":
logging.warning(
"Section length diagnostic | section=%s | actual=%s | target=%s | ratio=%.3f | status=%s | nonfatal=true",
name, actual, target, ratio, status,
)
return {
"scope": "section", "name": str(name), "actual_words": actual, "target_words": target,
"ratio": round(ratio, 4), "status": status, "recommendation": recommendation, "nonfatal": True,
}
@staticmethod
def _citation_eligible_section_ids(mode: str) -> set[str]:
return (
{"introduction", "theoretical_framing", "methods", "discussion"}
if str(mode).lower() == "asq"
else {"introduction", "theory", "methodology", "discussion"}
)
@classmethod
def _adaptive_citation_requirements(cls, tasks, mode: str):
"""Derive evidence demand and semantic-survival reserve from manuscript units."""
allowed = cls._citation_eligible_section_ids(mode)
minimums = {}
reserve_targets = {}
for task in tasks or []:
section_id = str(task.get("section_id") or "")
display_name = str(task.get("display_name") or "").strip()
if section_id not in allowed or not display_name:
continue
base, words_per_source = CITATION_ROLE_RULES.get(section_id, (2, 240))
target_words = max(1, int(task.get("target_words") or 1))
minimum = max(base, int(math.ceil(target_words / max(1, words_per_source))))
reserve = max(
minimum,
int(math.ceil(minimum / max(0.01, CITATION_SEMANTIC_SURVIVAL_RATIO))),
)
minimums[display_name] = minimum
reserve_targets[display_name] = reserve
return minimums, reserve_targets
@classmethod
def _top_level_citation_requirements(cls, tasks, mode: str):
"""Aggregate citation authority by academic section, never by technical chunk.
``Introduction (Part 1/2)`` and ``Introduction (Part 2/2)`` are generation
chunks, not separate scholarly evidence authorities. Planning may remain
unit-local, but fatal literature sufficiency is evaluated on the canonical
top-level academic section.
"""
allowed = cls._citation_eligible_section_ids(mode)
words_by_section = {}
for task in tasks or []:
section_id = str(task.get('section_id') or '')
if section_id not in allowed:
continue
words_by_section[section_id] = words_by_section.get(section_id, 0) + max(
1, int(task.get('target_words') or 1)
)
minimums = {}
reserve_targets = {}
for section_id, target_words in words_by_section.items():
base, words_per_source = CITATION_ROLE_RULES.get(section_id, (2, 240))
minimum = max(base, int(math.ceil(target_words / max(1, words_per_source))))
reserve = max(
minimum,
int(math.ceil(minimum / max(0.01, CITATION_SEMANTIC_SURVIVAL_RATIO))),
)
minimums[section_id] = minimum
reserve_targets[section_id] = reserve
return minimums, reserve_targets
@staticmethod
def _citation_unit_section_map(tasks):
return {
str(task.get('display_name') or ''): str(task.get('section_id') or '')
for task in (tasks or []) if str(task.get('display_name') or '').strip()
}
@staticmethod
def _top_level_unique_tag_counts(citation_plan, unit_section_map):
tags_by_section = {}
for unit_name, items in dict(citation_plan or {}).items():
section_id = str(unit_section_map.get(str(unit_name)) or '')
if not section_id:
continue
bucket = tags_by_section.setdefault(section_id, set())
for item in items or []:
tag = str((item or {}).get('tag') or '')
if tag:
bucket.add(tag)
return {section_id: len(tags) for section_id, tags in tags_by_section.items()}
@staticmethod
def _adaptive_literature_candidate_target(target_words: int, agent) -> int:
"""Adaptive retrieval aspiration; never a final reference-count quota."""
dynamic = max(24, int(math.ceil(max(1, int(target_words or 1)) / 90.0)))
configured_cap = max(1, int(getattr(agent, "lit_target_papers", dynamic) or dynamic))
return min(configured_cap, dynamic)
@staticmethod
def _citation_task_context(task, research_logic_contract="", synthesis_result="") -> str:
parts = [
str(task.get("display_name") or ""),
str(task.get("required_content") or ""),
" ".join(str(value) for value in (task.get("concepts") or []) if str(value).strip()),
str(research_logic_contract or "")[:1600],
str(synthesis_result or "")[:1600],
]
return re.sub(r"\s+", " ", " ".join(part for part in parts if part)).strip()
@staticmethod
def _literature_insufficient_error(shortfalls) -> RuntimeError:
details = "; ".join(
f"{name}={actual}/{required}"
for name, actual, required in (shortfalls or [])[:8]
)
return RuntimeError(
"E_LITERATURE_INSUFFICIENT: high-relevance claim-specific literature is insufficient "
f"for the planned manuscript units ({details}). Please revise the research topic or "
"keywords and try again; unrelated references will not be used to satisfy a quota."
)
@staticmethod
def _evidence_warning_detail() -> str:
return (
"Some externally oriented claims have limited direct high-relevance literature coverage. "
"This may reflect a newer or specialized topic, dispersed literature, search/keyword limits, "
"or limited prior evidence; this status does not determine the novelty, importance, or overall "
"research value of the topic. Unrelated sources were not used to satisfy a citation count."
)
def _record_evidence_coverage_event(self, state, shortfalls, *, phase: str, preview_status: str) -> None:
"""Persist a neutral evidence-coverage signal without stopping generation.
The event records only deterministic coverage counts. It never infers why
literature is sparse and never evaluates topic value. Duplicate events
are collapsed across resume/replay.
"""
rows = []
for name, actual, required in shortfalls or []:
rows.append({
"section": str(name or ""),
"actual": max(0, int(actual or 0)),
"required": max(0, int(required or 0)),
})
if not rows:
return
signature = (str(phase or ""), tuple((r["section"], r["actual"], r["required"]) for r in rows))
existing = list(state.get("evidence_coverage_events") or self._evidence_coverage_events or [])
seen = {
(str(item.get("phase") or ""), tuple(
(str(row.get("section") or ""), int(row.get("actual") or 0), int(row.get("required") or 0))
for row in (item.get("shortfalls") or []) if isinstance(row, dict)
))
for item in existing if isinstance(item, dict)
}
if signature not in seen:
existing.append({
"phase": str(phase or ""),
"shortfalls": rows,
"observed_at": datetime.now(timezone.utc).isoformat(),
})
self._evidence_coverage_events = existing
state["evidence_coverage_events"] = existing
state["evidence_risk_detected"] = True
self._preview_evidence_status = str(preview_status or "limited_risk")
self._write_preview_payload(force=True)
logging.warning(
"Evidence coverage warning | phase=%s | shortfalls=%s | nonfatal=true",
phase, rows,
)
@staticmethod
def _evidence_coverage_summary(*, status, early_risk, final_reference_count, minimums, validated_by_top_level, claims_by_top_level, events, method_authority_count=0, method_external_claims=0):
sections = []
for section_id, planned_minimum in dict(minimums or {}).items():
validated = len(set((validated_by_top_level or {}).get(section_id) or []))
external_claims = max(0, int((claims_by_top_level or {}).get(section_id, 0) or 0))
required = min(int(planned_minimum), external_claims) if external_claims else 0
sections.append({
"section": str(section_id),
"validated_direct_citations": validated,
"evidence_requirement": required,
"external_claims": external_claims,
"planning_minimum": int(planned_minimum),
})
return {
"status": str(status or "adequate"),
"early_risk_detected": bool(early_risk),
"final_reference_count": max(0, int(final_reference_count or 0)),
"sections": sections,
"methodology_authority": {
"validated_method_authority_citations": max(0, int(method_authority_count or 0)),
"methodology_external_claims": max(0, int(method_external_claims or 0)),
},
"events": list(events or []),
}
@staticmethod
def _paragraph_token_set(value):
text = re.sub(r'\[CITE-\d+\]', ' ', str(value or ''), flags=re.I)
return set(re.findall(r'[A-Za-z][A-Za-z0-9_-]{2,}', text.lower()))
@classmethod
def _remove_severe_cross_unit_duplicates(cls, candidate, prior_text):
"""Drop only very high-confidence cross-unit duplicate paragraphs."""
previous = [p.strip() for p in re.split(r'\n\s*\n', str(prior_text or '')) if len(p.split()) >= 55 and not p.lstrip().startswith('#')]
kept, diagnostics = [], []
for paragraph in re.split(r'(\n\s*\n)', str(candidate or '')):
if re.fullmatch(r'\n\s*\n', paragraph or ''):
kept.append(paragraph); continue
value = str(paragraph or '').strip()
if len(value.split()) < 55 or value.startswith('#'):
kept.append(paragraph); continue
tokens = cls._paragraph_token_set(value)
citations = set(re.findall(r'\[CITE-\d+\]', value, flags=re.I))
numerics = set(re.findall(r'(?<![A-Za-z])[-+]?\d+(?:\.\d+)?%?(?![A-Za-z])', value))
duplicate = None
for old in previous:
old_tokens = cls._paragraph_token_set(old)
union = tokens | old_tokens
similarity = len(tokens & old_tokens) / max(1, len(union))
if similarity < 0.90:
continue
old_citations = set(re.findall(r'\[CITE-\d+\]', old, flags=re.I))
old_numerics = set(re.findall(r'(?<![A-Za-z])[-+]?\d+(?:\.\d+)?%?(?![A-Za-z])', old))
# Never delete a paragraph carrying different evidence/facts.
if citations == old_citations and numerics == old_numerics:
duplicate = (similarity, old[:180])
break
if duplicate:
diagnostics.append({"similarity": round(duplicate[0], 4), "removed_preview": value[:180]})
continue
kept.append(paragraph)
previous.append(value)
return ''.join(kept).strip(), diagnostics
def _resolve_paths(self, items) -> list[str]:
output = []
for item in items or []:
path = (self.task_dir / str(item)).resolve()
if self.task_dir.resolve() not in path.parents:
raise RuntimeError("input_path_escape")
if path.is_file():
output.append(str(path))
return output
def _run_impl(self) -> dict[str, Any]:
req = self.request
mode = str(req.get("mode") or "").lower()
if mode not in {"amj", "asq"}:
raise RuntimeError("invalid_generation_mode")
target_words = int(req.get("target_words") or 0)
if not APP_CONFIG["target_words_min"] <= target_words <= APP_CONFIG["target_words_max"]:
raise RuntimeError("target_words_out_of_range")
ds_key = str(os.environ.get("DEEPSEEK_API_KEY") or "").strip()
s2_key = str(os.environ.get("S2_API_KEY") or "").strip()
if not ds_key:
raise RuntimeError("DEEPSEEK_API_KEY_missing")
if mode == "amj":
APP_CONFIG.update(AMJ_PROMPTS)
else:
APP_CONFIG.update(ASQ_PROMPTS)
agent = SSciPipelineAgent(ds_key, s2_key)
self.agent = agent
agent.set_cancel_checker(self.cancel_checker)
# DeepSeek/search activity marks the executor as genuinely alive; the
# heartbeat thread decides separately when to renew D1's lease.
agent.set_execution_progress_hook(lambda: self._progress_activity())
safe_chunk_words = int(agent.chunk_words_default)
quant_engine = get_quantitative_engine()
qual_engine = get_qualitative_engine(agent)
if self.confirmed_checkpoint_seq is None:
# Compatibility path for isolated unit helpers that do not represent
# a claimed production executor. Production always supplies Worker
# checkpoint metadata through task_executor.
state = self.checkpoints.load_latest() or {}
else:
state = self.checkpoints.load_confirmed() or {}
quality_diagnostics = list(state.get("quality_diagnostics") or [])
self._evidence_coverage_events = list(state.get("evidence_coverage_events") or [])
if str(state.get("evidence_status") or "") == "limited":
self._preview_evidence_status = "limited"
elif bool(state.get("evidence_risk_detected")):
self._preview_evidence_status = "limited_risk"
if state:
self._preview_english_text = str(state.get('full_draft_en') or '')
self._preview_target_text = str(state.get('full_draft_translated') or '')
logging.info(
"[SP_EXECUTOR] resume checkpoint seq=%s stage=%s",
state.get("checkpoint_seq"), state.get("checkpoint_stage"),
)
topic = str(req.get("topic") or "").strip()
keywords = str(req.get("keywords") or "").strip()
target_lang = str(req.get("target_lang") or "").strip()
output_suffix = str(req.get("output_suffix") or "Target").strip()
interface_language = str(req.get("interface_language") or "zh").strip()
reference_paths = self._resolve_paths(req.get("reference_files") or [])
data_paths = self._resolve_paths(req.get("data_files") or [])
interview_paths = self._resolve_paths(req.get("interview_files") or [])
base_evidence_contract = str(APP_CONFIG.get("evidence_integrity_contract") or "")
# Restore agent state before continuing any dependent step.
if isinstance(state.get("agent_state"), dict):
agent.load_state(state["agent_state"])
if state.get("english_finalized") and state.get("final_citation_ledger_version") != FINAL_CITATION_LEDGER_VERSION:
# R4-R3: an R4-R2 checkpoint may contain a correct resolved body but
# an empty References tail caused by rescanning already-resolved CITE
# tags. Re-enter only finalization/translation/package gates; completed
# English units and the same task/reservation remain authoritative.
logging.warning("Final citation ledger contract advanced; re-running finalization from frozen completed units.")
state["english_finalized"] = False
state["translation_complete"] = False
state["package_validated"] = False
if state.get("english_finalized") and state.get("english_section_seal_version") != ENGLISH_SECTION_SEAL_VERSION:
# R2 compatibility: upgrade old completed English sections locally.
# No writing-model call, new task, or new quota reservation is allowed.
logging.warning("English Section Integrity Seal contract advanced; re-validating frozen completed sections locally.")
state["english_finalized"] = False
state["translation_complete"] = False
state["package_validated"] = False
user_lit_context = str(state.get("user_lit_context") or "")
user_data_md = str(state.get("user_data_md") or "")
data_metadata = dict(state.get("data_metadata") or {})
if not state.get("evidence_ready"):
self._progress(3, "inputs_and_evidence")
if reference_paths:
user_lit_context = extract_text_from_files(reference_paths[:20])
all_paper_texts = [extract_text_from_files([path]) for path in reference_paths[:20]]
agent.global_memory["core_golden_metadata"] = extract_golden_metadata(agent, all_paper_texts)
if mode == "amj":
data_file = data_paths[0] if data_paths else None
_, user_data_md, _, data_metadata = quant_engine.resolve_empirical_data(
data_file, interface_language, topic=topic, keywords=keywords, agent=agent
)
else:
raw_interviews = extract_text_from_files(interview_paths[:1]) if interview_paths else ""
prewrite_started = time.monotonic()
prewrite_calls = agent.ds_calls
user_data_md, _, data_metadata = qual_engine.resolve_qualitative_data(topic, keywords, raw_interviews)
agent.record_prewrite_stage("qualitative_packet", prewrite_started, prewrite_calls)
state.update(
{
"topic": topic,
"keywords": keywords,
"target_lang": target_lang,
"target_words": target_words,
"mode": mode,
"interface_language": interface_language,
"user_lit_context": user_lit_context,
"user_data_md": user_data_md,
"data_metadata": data_metadata,
"agent_state": agent.export_state(),
"evidence_ready": True,
}
)
self._checkpoint("evidence_ready", state, 6)
if mode == "amj":
current_quant_ledger = dict(data_metadata.get("quantitative_evidence_ledger") or {})
expected_quant_version = str(getattr(quant_engine, "QUANTITATIVE_EVIDENCE_LEDGER_VERSION", ""))
if current_quant_ledger.get("version") != expected_quant_version:
data_metadata["quantitative_evidence_ledger"] = quant_engine.build_quantitative_evidence_ledger(
data_metadata, user_data_md
)
state["data_metadata"] = data_metadata
logging.warning(
"Quantitative Evidence Ledger contract advanced; rebuilt locally version=%s",
expected_quant_version,
)
self._checkpoint("quantitative_evidence_ledger_upgraded", state, 6)
self._qualitative_quote_sources = (
self._qualitative_quote_source_map(data_metadata) if mode == "asq" else {}
)
# ASQ planning never needs access to verbatim quotations. Keep the
# research-logic/front-matter stage on a quote-free internal-evidence
# summary; verbatim text is disclosed later only to its owning Findings
# unit (or the appendix).
planning_data_md = (
qual_engine.build_scoped_evidence_md(data_metadata, include_themes=True)
if mode == "asq"
else user_data_md
)
refs_text = str(state.get("refs_text") or "")
if not refs_text:
self._progress(8, "S2 + OpenAlex")
literature_candidate_target = self._adaptive_literature_candidate_target(target_words, agent)
refs_text = agent.fetch_references_parallel(
topic,
keywords,
min_required=literature_candidate_target,
progress_callback=lambda: self._progress(9, "S2 + OpenAlex"),
)
if not agent.citation_map:
self._record_evidence_coverage_event(
state, [("retrieved literature pool", 0, 1)],
phase="retrieval_pool_empty", preview_status="limited_risk",
)
logging.warning(
"No directly validated literature was retrieved; continuing in evidence-limited mode without padding references."
)
state["literature_candidate_target"] = literature_candidate_target
if user_lit_context:
core_refs = agent.parse_user_core_literature(user_lit_context)
if core_refs:
refs_text = (
"【USER PROVIDED CORE LITERATURE】\n" + core_refs
+ "\n\n【RETRIEVED LITERATURE】\n" + refs_text
)
state.update({"refs_text": refs_text, "agent_state": agent.export_state()})
self._checkpoint("literature_ready", state, 11)
rag_engine = initialize_rag([value for value in (user_lit_context, refs_text) if value])
synthesis_result = str(state.get("synthesis_result") or "")
if not synthesis_result:
self._progress(12, "literature_synthesis")
prewrite_started = time.monotonic()
prewrite_calls = agent.ds_calls
if not str(refs_text or "").strip() and not str(user_lit_context or "").strip():
synthesis_result = (
"EVIDENCE-LIMITED CONTEXT: no directly validated external literature is currently available. "
"Continue with the frozen study evidence, cautious conceptual reasoning, and general scholarly knowledge, "
"but do not invent citations or bibliographic metadata and do not present general model knowledge as retrieved evidence."
)
else:
synthesis_result = self._stream_text(
agent.stream_synthesize_literature(topic, refs_text, user_core_literature=user_lit_context)
)
agent.record_prewrite_stage("literature_synthesis", prewrite_started, prewrite_calls)
synthesis_result = re.sub(r"<think>.*?</think>", "", synthesis_result, flags=re.DOTALL).strip()
state.update({"synthesis_result": synthesis_result, "agent_state": agent.export_state()})
self._checkpoint("synthesis_ready", state, 13)
adaptive_structure = state.get("adaptive_structure")
if not isinstance(adaptive_structure, list) or not adaptive_structure:
self._progress(14, "research_logic")
prewrite_started = time.monotonic()
prewrite_calls = agent.ds_calls
research_logic_response = self._stream_text(
agent.stream_generate_research_logic(
topic, keywords, synthesis_result, refs_text,
data_evidence=planning_data_md, mode=mode,
)
)
agent.record_prewrite_stage("research_logic", prewrite_started, prewrite_calls)
research_logic_contract = re.sub(
r"<think>.*?</think>", "", research_logic_response, flags=re.DOTALL
).strip()
if not research_logic_contract:
raise RuntimeError("research_logic_contract_missing")
agent.global_memory["research_logic_contract"] = research_logic_contract
self._progress(14, "working_front_matter")
working_front = agent.generate_working_front_matter(
topic, keywords, research_logic_contract, synthesis_result,
data_evidence=planning_data_md, mode=mode,
)
working_abstract = str(working_front.get("abstract") or "").strip()
working_keywords = list(working_front.get("keywords") or [])
if not working_abstract or not working_keywords:
raise RuntimeError("working_front_matter_missing")
working_abstract, front_fact_rows = self._repair_validate_study_fact_contract(
mode, agent, quant_engine, working_abstract, data_metadata,
section_name="Working Abstract",
)
if not working_abstract:
raise RuntimeError("working_front_matter_empty_after_study_fact_contract")
if front_fact_rows:
logging.warning(
"Working Abstract study-fact repair | mode=%s count=%s types=%s",
mode, len(front_fact_rows),
sorted({str(row.get("type") or "") for row in front_fact_rows}),
)
quality_diagnostics.append({
"scope":"working_abstract_study_fact_integrity", "name":"Working Abstract",
"status":"deterministic_repair", "count":len(front_fact_rows),
"examples":front_fact_rows[:3],
})
agent.global_memory["working_abstract"] = working_abstract
self._progress(14, "outline_planning")
plan = agent.generate_outline(topic, keywords, mode=mode, data_metadata=data_metadata)
en_topic = str(plan.get("en_topic", topic))
structure = plan.get("structure", [])
full_draft_en = (
f"# {en_topic}\n\n## Abstract\n{working_abstract}\n\n"
f"## Keywords\n{', '.join(working_keywords)}\n\n"
)
# Publish the Working Abstract/Keywords immediately, before JIT and
# citation planning, so the user sees the manuscript anchor before
# Introduction while those longer planning stages continue.
self._publish_preview(full_draft_en, "working_front_matter", force=True)
front_budget = max(180, int(APP_CONFIG.get("word_budget", {}).get("front_matter_words", 220)))
body_target_words = max(1000, target_words - front_budget)
adaptive_structure = agent.build_generation_tasks(structure, body_target_words, safe_chunk_words)
for task in adaptive_structure:
task["is_first"] = bool(task.get("section_first"))
quote_invocation_plan = (
self._build_qualitative_quote_invocation_plan(adaptive_structure, data_metadata)
if mode == "asq" else {}
)
jit_texts = agent.prefetch_jit_literature(topic, keywords, adaptive_structure)
if jit_texts:
rag_engine._initialize_chunks([jit_texts])
self._progress(15, "citation_planning")
citation_minimums, citation_reserve_targets = self._adaptive_citation_requirements(
adaptive_structure, mode
)
citation_top_level_minimums, citation_top_level_reserve_targets = self._top_level_citation_requirements(
adaptive_structure, mode
)
citation_unit_section_map = self._citation_unit_section_map(adaptive_structure)
citation_plan_target = sum(citation_top_level_reserve_targets.values())
citation_plan = agent.build_claim_evidence_plan(
adaptive_structure, topic, synthesis_result, mode=mode,
target_unique=max(1, citation_plan_target),
)
# Early neutral risk signal: evaluate the high-relevance plan before
# reserve filling/JIT can make a raw candidate count look healthier
# than the claim-specific plan actually is. This is advisory only.
initial_top_level_counts = self._top_level_unique_tag_counts(
citation_plan, citation_unit_section_map
)
initial_shortfalls = [
(section_id, int(initial_top_level_counts.get(section_id, 0)), required)
for section_id, required in citation_top_level_minimums.items()
if int(initial_top_level_counts.get(section_id, 0)) < int(required)
]
if initial_shortfalls:
self._record_evidence_coverage_event(
state, initial_shortfalls, phase="initial_claim_specific_plan",
preview_status="limited_risk",
)
# Fill unit-specific reserve gaps before expensive drafting. Existing
# retrieved evidence is preferred; bounded contextual JIT is last resort.
planned_tags = {
str(item.get("tag") or "")
for items in citation_plan.values()
for item in items
if item.get("tag")
}
task_by_name = {
str(task.get("display_name") or ""): task
for task in adaptive_structure
if str(task.get("display_name") or "")
}
for display_name, reserve_target in citation_reserve_targets.items():
planned_items = citation_plan.setdefault(display_name, [])
shortfall = max(0, reserve_target - len(planned_items))
if shortfall <= 0:
continue
section_context = self._citation_task_context(
task_by_name.get(display_name, {}), research_logic_contract, synthesis_result
)
recovery_items = agent.select_contextual_citation_recovery_items(
[], section_name=display_name, section_context=section_context,
needed=shortfall, excluded_tags=planned_tags,
)
for item in recovery_items:
tag = str(item.get("tag") or "")
if tag and tag not in planned_tags:
planned_items.append(item)
planned_tags.add(tag)
# Do not trigger external retrieval merely because a technical
# generation chunk is below a word-derived target. Fatal evidence
# authority belongs to the canonical top-level academic section.
top_level_counts = self._top_level_unique_tag_counts(citation_plan, citation_unit_section_map)
for section_id, required in citation_top_level_minimums.items():
deficit = max(0, int(required) - int(top_level_counts.get(section_id, 0)))
if deficit <= 0:
continue
section_units = [
name for name, mapped in citation_unit_section_map.items() if mapped == section_id
]
if not section_units:
continue
primary_unit = section_units[0]
section_context = " ".join(
self._citation_task_context(task_by_name.get(name, {}), research_logic_contract, synthesis_result)
for name in section_units
)
jit_items = agent.fetch_contextual_jit_recovery_items(
topic=topic, section_name=section_id, section_context=section_context,
needed=deficit, excluded_tags=planned_tags,
)
planned_items = citation_plan.setdefault(primary_unit, [])
for item in jit_items:
tag = str(item.get("tag") or "")
if tag and tag not in planned_tags:
planned_items.append(item)
planned_tags.add(tag)
top_level_counts = self._top_level_unique_tag_counts(citation_plan, citation_unit_section_map)
shortfalls = [
(section_id, int(top_level_counts.get(section_id, 0)), required)
for section_id, required in citation_top_level_minimums.items()
if int(top_level_counts.get(section_id, 0)) < int(required)
]
if shortfalls:
self._record_evidence_coverage_event(
state, shortfalls, phase="bounded_retrieval_complete",
preview_status="limited_risk",
)
logging.warning(
"Top-level citation feasibility limited: minimum_total=%s reserve_target=%s planned_unique=%s sections=%s nonfatal=true",
sum(citation_top_level_minimums.values()), citation_plan_target, len(planned_tags), len(citation_top_level_minimums),
)
else:
logging.info(
"Top-level citation feasibility passed: minimum_total=%s reserve_target=%s planned_unique=%s sections=%s",
sum(citation_top_level_minimums.values()), citation_plan_target, len(planned_tags), len(citation_top_level_minimums),
)
logging.info(
"Top-level citation planning complete: minimum_total=%s reserve_target=%s planned_unique=%s sections=%s",
sum(citation_top_level_minimums.values()), citation_plan_target, len(planned_tags), len(citation_top_level_minimums),
)
canonical_concept_ledger = self._build_canonical_concept_ledger(
mode, data_metadata, keywords=keywords, adaptive_structure=adaptive_structure,
research_logic_contract=research_logic_contract,
)
state.update(
{
"adaptive_structure": adaptive_structure,
"quote_invocation_plan": quote_invocation_plan,
"citation_plan": citation_plan,
"citation_minimums": citation_minimums,
"citation_reserve_targets": citation_reserve_targets,
"citation_top_level_minimums": citation_top_level_minimums,
"citation_top_level_reserve_targets": citation_top_level_reserve_targets,
"citation_unit_section_map": citation_unit_section_map,
"citation_validated_tags_by_unit": {},
"citation_required_claim_counts_by_unit": {},
"section_integrity_seals": {},
"english_section_seal_version": ENGLISH_SECTION_SEAL_VERSION,
"research_logic_contract": research_logic_contract,
"working_abstract": working_abstract,
"working_keywords": working_keywords,
"front_matter_phase": "working",
"body_target_words": body_target_words,
"full_draft_en": full_draft_en,
"full_draft_translated": "",
"translation_sections": {},
"translation_glossary": {},
"canonical_concept_ledger": canonical_concept_ledger,
"completed_sections": [],
"context_tail": "",
"accumulated_deviation": 0.0,
"agent_state": agent.export_state(),
"outline_ready": True,
}
)
self._publish_preview(full_draft_en, "outline_ready", force=True)
self._checkpoint("outline_ready", state, 15)
else:
full_draft_en = str(state.get("full_draft_en") or "")
# JIT text itself is already represented in agent citation state;
# refs + user literature rebuild a safe RAG baseline on resume.
completed_sections = list(state.get("completed_sections") or [])
context_tail = str(state.get("context_tail") or "")
accumulated_deviation = float(state.get("accumulated_deviation") or 0.0)
research_logic_contract = str(
state.get("research_logic_contract") or agent.global_memory.get("research_logic_contract") or ""
)
citation_plan = state.get("citation_plan") if isinstance(state.get("citation_plan"), dict) else {}
citation_minimums = dict(state.get("citation_minimums") or {})
citation_reserve_targets = dict(state.get("citation_reserve_targets") or {})
citation_top_level_minimums = dict(state.get("citation_top_level_minimums") or {})
citation_top_level_reserve_targets = dict(state.get("citation_top_level_reserve_targets") or {})
citation_unit_section_map = dict(state.get("citation_unit_section_map") or {})
citation_required_claim_counts_by_unit = {
str(name): int(count or 0)
for name, count in dict(state.get("citation_required_claim_counts_by_unit") or {}).items()
}
citation_validated_tags_by_unit = {
str(name): list(tags or [])
for name, tags in dict(state.get("citation_validated_tags_by_unit") or {}).items()
}
section_integrity_seals = {
str(key): dict(record or {})
for key, record in dict(state.get("section_integrity_seals") or {}).items()
if isinstance(record, dict)
}
if not citation_minimums:
citation_minimums, citation_reserve_targets = self._adaptive_citation_requirements(
adaptive_structure, mode
)
if not citation_top_level_minimums:
citation_top_level_minimums, citation_top_level_reserve_targets = self._top_level_citation_requirements(
adaptive_structure, mode
)
if not citation_unit_section_map:
citation_unit_section_map = self._citation_unit_section_map(adaptive_structure)
translation_sections = dict(state.get('translation_sections') or {})
translation_glossary = dict(state.get('translation_glossary') or {})
canonical_concept_ledger = state.get('canonical_concept_ledger')
if not isinstance(canonical_concept_ledger, dict) or not canonical_concept_ledger.get('items'):
canonical_concept_ledger = self._build_canonical_concept_ledger(
mode, data_metadata, keywords=keywords, adaptive_structure=adaptive_structure,
research_logic_contract=research_logic_contract,
)
state['canonical_concept_ledger'] = canonical_concept_ledger
working_abstract = str(state.get("working_abstract") or "").strip()
working_keywords = list(state.get("working_keywords") or [])
if working_abstract and not state.get("english_finalized"):
checked_working_abstract, resumed_front_rows = self._repair_validate_study_fact_contract(
mode, agent, quant_engine, working_abstract, data_metadata,
section_name="Working Abstract",
)
if not checked_working_abstract:
raise RuntimeError("working_front_matter_empty_after_study_fact_contract")
if checked_working_abstract != working_abstract or resumed_front_rows:
working_abstract = checked_working_abstract
state["working_abstract"] = working_abstract
if str(state.get("full_draft_en") or "").strip():
state["full_draft_en"] = replace_abstract(str(state.get("full_draft_en") or ""), working_abstract)
full_draft_en = str(state["full_draft_en"] or "")
quality_diagnostics.append({
"scope":"working_abstract_study_fact_integrity", "name":"Working Abstract",
"status":"resume_deterministic_repair", "count":len(resumed_front_rows),
"examples":resumed_front_rows[:3],
})
state["quality_diagnostics"] = quality_diagnostics
self._checkpoint("working_front_matter_fact_reconciled", state, 15)
if working_abstract:
agent.global_memory["working_abstract"] = working_abstract
if self._preview_target_text == '':
self._preview_target_text = str(state.get('full_draft_translated') or '')
# P0-R1.1-R2 compatibility migration: old checkpoints predate Section
# Integrity Seals. Reconcile only already-completed English sections with
# deterministic local contracts, seal them, and invalidate target cache
# only when the exact English section changes. Never call the writer.
seal_migration = False
seal_text_changed = False
for section_key, section_title in self._unique_top_level_sections(adaptive_structure):
if not self._section_is_complete(adaptive_structure, completed_sections, section_key):
continue
section_text = self._extract_top_level_section(full_draft_en, section_title)
if not section_text:
continue
existing_seal = dict(section_integrity_seals.get(section_key) or {})
if (
existing_seal.get("version") == ENGLISH_SECTION_SEAL_VERSION
and existing_seal.get("sha256") == self._section_integrity_hash(section_text)
):
continue
migrated_text, enum_rows = agent.repair_and_validate_enumeration_contract(
section_text, section_name=f"Resume sealed section: {section_title}"
)
migrated_text, fact_rows = self._repair_validate_study_fact_contract(
mode, agent, quant_engine, migrated_text, data_metadata,
section_name=f"Resume sealed section: {section_title}",
)
if migrated_text.strip() != section_text.strip():
full_draft_en = self._replace_top_level_section(
full_draft_en, section_title, migrated_text
)
translation_sections.pop(section_key, None)
seal_text_changed = True
quality_diagnostics.append({
"scope":"english_section_seal_migration", "name":section_title,
"status":"deterministic_repair", "count":len(enum_rows) + len(fact_rows),
"examples":(list(enum_rows or []) + list(fact_rows or []))[:3],
})
section_text = self._extract_top_level_section(full_draft_en, section_title)
section_integrity_seals[section_key] = self._build_section_integrity_seal(
section_key, section_title, section_text
)
seal_migration = True
if seal_migration:
# Rebuild mutable manuscript memory only from sealed English text so
# rejected/raw claims cannot propagate into later sections.
for memory_key in ("hypotheses", "variables", "model_summary", "method_summary"):
agent.global_memory[memory_key] = ""
last_sealed = ""
for section_key, section_title in self._unique_top_level_sections(adaptive_structure):
if not self._section_is_complete(adaptive_structure, completed_sections, section_key):
continue
sealed_text = self._extract_top_level_section(full_draft_en, section_title)
if sealed_text:
agent._extract_memory_from_text(sealed_text, section_title)
last_sealed = sealed_text
if last_sealed:
context_tail = " ".join(last_sealed.split()[-200:])
state.update({
"full_draft_en": full_draft_en,
"translation_sections": translation_sections,
"section_integrity_seals": section_integrity_seals,
"english_section_seal_version": ENGLISH_SECTION_SEAL_VERSION,
"context_tail": context_tail,
"quality_diagnostics": quality_diagnostics,
"agent_state": agent.export_state(),
})
if seal_text_changed:
state["full_draft_translated"] = self._assemble_partial_translation(
translation_sections, adaptive_structure, include_front=True
)
self._checkpoint("english_section_seal_migrated", state, 15)
# Target-language work remains Executor-owned but no longer blocks the
# English writer. A dedicated single translation agent/worker keeps
# glossary order deterministic and avoids sharing mutable writer state.
translation_agent = SSciPipelineAgent(ds_key, s2_key)
translation_agent.set_execution_progress_hook(lambda: self._progress_activity())
translation_agent.set_cancel_checker(lambda: self.cancel_checker())
seeded_glossary = translation_agent.seed_canonical_concept_glossary(
canonical_concept_ledger, target_lang, translation_glossary
)
if seeded_glossary != translation_glossary:
translation_glossary = dict(seeded_glossary)
state['translation_glossary'] = translation_glossary
state['canonical_concept_ledger'] = canonical_concept_ledger
self._checkpoint('canonical_concept_ledger_ready', state, 15)
current_entity_ledger_version = str(
getattr(translation_agent, 'PROTECTED_ENTITY_LEDGER_VERSION', 'protected-entity-ledger-v1')
or 'protected-entity-ledger-v1'
)
stale_entity_units = [
key for key, record in translation_sections.items()
if isinstance(record, dict)
and record.get('translated_text')
and record.get('qa_status') in {'passed', 'passed_with_warning'}
and record.get('protected_entity_ledger_version') != current_entity_ledger_version
]
if stale_entity_units:
logging.warning(
'Protected Entity Ledger contract advanced; invalidating target-language cache only | units=%s version=%s',
stale_entity_units[:8], current_entity_ledger_version,
)
state['translation_complete'] = False
state['package_validated'] = False
state['full_draft_translated'] = ''
self._preview_target_text = ''
self._checkpoint('protected_entity_ledger_advanced', state, 15)
self._start_translation_worker(
translation_agent,
translation_sections=translation_sections,
translation_glossary=translation_glossary,
adaptive_structure=adaptive_structure,
target_lang=target_lang,
)
# Capture a dedicated abort Event, not the worker lifecycle stop Event.
# The worker is gracefully closed before the strict final translation
# gate, but the same translation agent remains valid for deferred/final
# units. Abortive shutdown still cancels any in-flight target call.
translation_abort_event = self._translation_worker.cancel_event
translation_agent.set_cancel_checker(
lambda: self.cancel_checker() or translation_abort_event.is_set()
)
# Backward-compatible resume path for a pre-R3.2.8.3 checkpoint that has
# an outline but no Working Abstract yet. Generate it once, checkpoint it,
# and reuse it thereafter; never regenerate an unchanged anchor on resume.
if not working_abstract and not state.get("english_finalized"):
working_front = agent.generate_working_front_matter(
topic, keywords, research_logic_contract, synthesis_result,
data_evidence=planning_data_md, mode=mode,
)
working_abstract = str(working_front.get("abstract") or "").strip()
working_keywords = list(working_front.get("keywords") or [])
if not working_abstract or not working_keywords:
raise RuntimeError("working_front_matter_missing")
working_abstract, front_fact_rows = self._repair_validate_study_fact_contract(
mode, agent, quant_engine, working_abstract, data_metadata,
section_name="Working Abstract",
)
if not working_abstract:
raise RuntimeError("working_front_matter_empty_after_study_fact_contract")
if front_fact_rows:
logging.warning(
"Working Abstract resume study-fact repair | mode=%s count=%s types=%s",
mode, len(front_fact_rows),
sorted({str(row.get("type") or "") for row in front_fact_rows}),
)
quality_diagnostics.append({
"scope":"working_abstract_study_fact_integrity", "name":"Working Abstract",
"status":"deterministic_repair", "count":len(front_fact_rows),
"examples":front_fact_rows[:3],
})
agent.global_memory["working_abstract"] = working_abstract
full_draft_en = replace_abstract(full_draft_en, working_abstract)
full_draft_en = self._replace_keywords(full_draft_en, working_keywords)
state.update({
"working_abstract": working_abstract,
"working_keywords": working_keywords,
"front_matter_phase": "working",
"full_draft_en": full_draft_en,
"agent_state": agent.export_state(),
})
self._publish_preview(full_draft_en, "working_front_matter", force=True)
self._checkpoint("working_front_matter_ready", state, 15)
# Queue the QA-frozen Working front matter. Translation is allowed to
# overlap later English work, but only frozen English units may enter
# this Executor-owned ordered worker.
if working_abstract and not state.get("english_finalized"):
section_order_for_front = self._unique_top_level_sections(adaptive_structure)
if section_order_for_front:
first_section_title = section_order_for_front[0][1]
working_front_source = self._extract_front_matter(full_draft_en, first_section_title)
if not working_front_source and not completed_sections:
# Fresh tasks contain only Title/Abstract/Keywords here; the
# first body heading does not exist yet. Preserve legacy
# resume behavior when completed body sections are present.
working_front_source = str(full_draft_en or '').strip()
self._enqueue_translation_unit(
translation_agent,
unit_key='__front_matter__', unit_name='Working Front Matter',
source_text=working_front_source,
translation_sections=translation_sections,
english_commit_text=working_front_source,
)
# Resume/idempotency: completed English top-level sections whose current
# source-hash translation is absent/stale are queued exactly once.
for section_key, section_title in self._unique_top_level_sections(adaptive_structure):
if not self._section_is_complete(adaptive_structure, completed_sections, section_key):
continue
frozen_section = self._extract_top_level_section(full_draft_en, section_title)
self._enqueue_translation_unit(
translation_agent,
unit_key=section_key,
unit_name=section_title,
source_text=frozen_section,
translation_sections=translation_sections,
english_commit_text=self._english_prefix_through_top_level(full_draft_en, section_title),
)
translation_sections, translation_glossary = self._collect_translation_results(
translation_sections, translation_glossary, block=False
)
if not citation_plan:
self._progress(15, "citation_planning")
citation_plan = agent.build_claim_evidence_plan(
adaptive_structure, topic, synthesis_result, mode=mode,
target_unique=max(1, sum(citation_top_level_reserve_targets.values())),
)
top_level_counts = self._top_level_unique_tag_counts(citation_plan, citation_unit_section_map)
shortfalls = [
(section_id, int(top_level_counts.get(section_id, 0)), required)
for section_id, required in citation_top_level_minimums.items()
if int(top_level_counts.get(section_id, 0)) < int(required)
]
if shortfalls:
self._record_evidence_coverage_event(
state, shortfalls, phase="resume_claim_specific_plan",
preview_status="limited_risk",
)
state["citation_plan"] = citation_plan
state["citation_minimums"] = citation_minimums
state["citation_reserve_targets"] = citation_reserve_targets
state["citation_top_level_minimums"] = citation_top_level_minimums
state["citation_top_level_reserve_targets"] = citation_top_level_reserve_targets
state["citation_unit_section_map"] = citation_unit_section_map
state["citation_required_claim_counts_by_unit"] = citation_required_claim_counts_by_unit
state["citation_validated_tags_by_unit"] = citation_validated_tags_by_unit
quote_invocation_plan = (
dict(state.get("quote_invocation_plan") or {}) if mode == "asq" else {}
)
if mode == "asq":
if not quote_invocation_plan:
quote_invocation_plan = self._build_qualitative_quote_invocation_plan(
adaptive_structure, data_metadata
)
state["quote_invocation_plan"] = quote_invocation_plan
self._checkpoint("quote_plan_ready", state, 15)
self._validate_qualitative_quote_invocation_plan(
quote_invocation_plan, adaptive_structure, data_metadata
)
total_steps = len(adaptive_structure)
for sec in adaptive_structure:
display_name = sec["display_name"]
if display_name in completed_sections:
continue
if self.cancel_checker():
raise RuntimeError("task_cancel_requested")
remaining = sum(1 for item in adaptive_structure if item["display_name"] not in completed_sections)
adjusted_target = self._bounded_target(
int(sec["target_words"]), 0.0, remaining, safe_chunk_words
)
sec_title = sec["section"]
subsection = sec.get("subsection")
is_quarantined = any(
word in sec_title.lower()
for word in ["abstract", "keyword", "conclusion", "references", "appendix"]
)
planned_items = list(citation_plan.get(display_name, []))
assigned_tag_list = [str(item.get("tag") or "") for item in planned_items if item.get("tag")]
claim_plan_text = agent.format_claim_evidence_plan(planned_items)
section_id = str(sec.get("section_id") or "")
section_role = str(
APP_CONFIG.get("section_reasoning_profiles", {}).get(mode, {}).get(section_id) or ""
)
mode_directive = ""
if section_id in {"theory", "theoretical_framing"}:
mode_directive = str(APP_CONFIG.get("hypothesis_development_directive") or "")
elif section_id in {"methodology", "methods"}:
mode_directive = str(APP_CONFIG.get("methodological_defense_directive") or "")
elif section_id == "discussion":
mode_directive = str(APP_CONFIG.get("discussion_depth_directive") or "")
elif section_id == "conclusion":
mode_directive = str(APP_CONFIG.get("conclusion_anchor_directive") or "")
if is_quarantined or section_id in {"results", "findings"}:
citation_rule = (
"NO EXTERNAL LITERATURE CITATIONS ALLOWED IN THIS UNIT. "
"Report only frozen empirical/qualitative evidence and the interpretation permitted by the section role."
)
else:
citation_rule = (
"Use the citation-first claim-evidence plan below as bounded evidence options, not as a claim quota. "
"Do not create a new claim merely to consume an assigned citation. Use a source only when it directly supports a claim independently required by the frozen research logic, section role, or already established manuscript argument; omit any item that would introduce another application domain or mutate the study's facts. "
"When a source is used, keep the claim no broader than the supplied evidence and attach exactly its assigned [CITE-XX] tag. "
"Never stack sources, substitute tags, or use literature as evidence for this study's own facts.\n"
f"<CLAIM_EVIDENCE_PLAN>\n{claim_plan_text}\n</CLAIM_EVIDENCE_PLAN>"
)
if not claim_plan_text.strip():
citation_rule += (
"\nEVIDENCE-LIMITED WRITING RULE: no directly validated source is assigned to this unit. "
"Continue the scholarly argument using frozen study evidence, cautious conceptual reasoning, and general scholarly knowledge. "
"Do not invent citations, authors, titles, DOI values, or bibliographic metadata; do not describe general model knowledge as if it were retrieved or directly validated literature. "
"Use appropriately qualified language for propositions, interpretations, and research opportunities."
)
if mode == "asq" and section_id not in {"findings", "appendix"}:
citation_rule += (
"\nASQ EVIDENCE-CHANNEL RULE: A sentence whose factual subject is this study's setting, participants, interviews, data sources, cases, actors, observations, events, quotations, analytic steps, or empirical findings must not carry an external [CITE-XX] tag. "
"Place any prior-literature or methodological-rationale claim in a separate sentence and cite only that external claim."
)
memory_context = agent._build_memory_context(sec_title, 2500)
memory_block = f"\n<GLOBAL_MEMORY>\n{memory_context}\n</GLOBAL_MEMORY>\n" if memory_context else ""
snippets = rag_engine.get_relevant_context(display_name) if rag_engine is not None else ""
if snippets:
memory_block += f"\n<TARGETED_RAG>\n{snippets}\n</TARGETED_RAG>\n"
heading_directive = "Do not output the section heading; it will be inserted by the system."
if subsection:
heading_directive += (
f" Do not output the subsection heading '{subsection}'; it will also be inserted by the system. "
"Use level-3 headings only when analytically necessary."
)
elif sec_title.lower() in {"introduction", "conclusion"}:
heading_directive += " Do not use additional headings."
else:
heading_directive += (
" Use level-2 or level-3 headings only when they clarify the argument; "
"never use level-4 headings or numbered headings."
)
table_rule = (
"Present only frozen statistical evidence in standard Markdown tables with plain-text captions."
if sec["is_method_or_result"] and mode == "amj" else ""
)
concepts = ", ".join(sec.get("concepts") or [])
theme_evidence = [item for item in (sec.get("theme_evidence") or []) if isinstance(item, dict)]
evidence_profiles = []
for item in theme_evidence:
stage = str(item.get("stage") or "Not specified").strip()
role = str(item.get("evidence_role") or "typical").strip()
case_label = str(item.get("case_label") or "Not specified").strip()
evidence_profiles.append(f"stage={stage}; case={case_label}; role={role}")
planned_quote_count = sum(
1 for record in quote_invocation_plan.values()
if str(record.get("render_task_key") or "") == self._qualitative_task_key(sec)
)
qualitative_rule = (
f"Assigned qualitative concepts: {concepts}. Assigned evidence profiles: {' | '.join(evidence_profiles)}. "
f"The Executor has frozen {planned_quote_count} quotation invocation(s) for this task. "
"Use the assigned evidence analytically, but output narrative prose only: do not reproduce any verbatim quotation and do not output any QUOTE-XX identifier. "
"The Executor inserts the frozen quotations deterministically after narrative/citation QA. Preserve stage/case/deviant meaning and use deviant evidence to qualify rather than erase the pattern."
if concepts or evidence_profiles else ""
)
scoped_data_md = user_data_md
if mode == "asq":
if section_id == "appendix":
# The appendix is the only non-owning unit allowed to see
# the full frozen evidence table. It is excluded from the
# body duplicate-quote gate by design.
scoped_data_md = user_data_md
elif section_id == "findings":
current_theme = str(sec.get("theme_title") or "").strip()
scoped_data_md = qual_engine.build_scoped_evidence_md(
data_metadata,
include_themes=True,
theme_titles=[current_theme] if current_theme else None,
assigned_evidence=theme_evidence,
include_quotes=bool(theme_evidence),
include_quote_ids=False,
)
elif section_id == "discussion":
scoped_data_md = qual_engine.build_scoped_evidence_md(
data_metadata,
include_themes=True,
)
else:
scoped_data_md = qual_engine.build_scoped_evidence_md(
data_metadata,
include_themes=False,
)
evidence_packet = (
f"{base_evidence_contract}\n<FROZEN_RESEARCH_EVIDENCE>\n{scoped_data_md}\n</FROZEN_RESEARCH_EVIDENCE>"
if scoped_data_md else base_evidence_contract
)
completed_label = ", ".join(completed_sections)
anti_repeat = (
f"Already completed: {completed_label}. Do not restate their background, gap, or contribution."
if completed_label else ""
)
if re.search(r"\(Part\s+2/2\)", display_name, flags=re.I):
anti_repeat += " Part 2/2 must extend and complement Part 1/2; do not recap its background, mechanism explanation, evidence, or contribution framing."
voice = str(APP_CONFIG.get("journal_voice_profiles", {}).get(mode) or "")
sys_prompt = (
APP_CONFIG.get("sys_academic_writer", "") + "\n"
+ evidence_packet + "\n" + voice + "\n"
+ APP_CONFIG.get("deep_refinement_directive", "")
)
word_budget = APP_CONFIG.get("word_budget", {})
min_ratio = float(word_budget.get("section_min_ratio", 0.92))
max_ratio = float(word_budget.get("section_max_ratio", 1.08))
prompt_context_tail = context_tail[-2500:]
if mode == "asq":
prompt_context_tail = self._redact_qualitative_quotes(prompt_context_tail, data_metadata)
user_prompt = f"""Write the assigned English manuscript unit.
Section: {sec_title}
Subsection: {subsection or 'None'}
Section reasoning role: {section_role}
Additional mode directive: {mode_directive}
Planning length: approximately {int(adjusted_target * min_ratio)}-{int(adjusted_target * max_ratio)} words. This is guidance, not a content quota. Prioritize Conciseness, Accuracy, and Rigor; never add filler or remove useful reasoning solely to hit the range.
{heading_directive}
{citation_rule}
{table_rule}
{str(sec.get('required_content') or '')}
{qualitative_rule}
{anti_repeat}
{self._canonical_concept_writer_directive(canonical_concept_ledger)}
<PROVISIONAL_RESEARCH_LOGIC>
{research_logic_contract}
</PROVISIONAL_RESEARCH_LOGIC>
<WORKING_ABSTRACT_ANCHOR>
{working_abstract}
</WORKING_ABSTRACT_ANCHOR>
ANCHOR PRECEDENCE: Frozen research evidence and the PROVISIONAL_RESEARCH_LOGIC override the Working Abstract if any tension appears. The Working Abstract is continuity guidance only and must never force a result.
{memory_block}
Literature synthesis: {synthesis_result}
<FROZEN_RESEARCH_EVIDENCE>
{scoped_data_md}
</FROZEN_RESEARCH_EVIDENCE>
Previous tail: {prompt_context_tail}
Output only final academic prose. Do not report the word count."""
if agent._count_tokens(sys_prompt) + agent._count_tokens(user_prompt) > 60000:
user_prompt = agent._truncate_context(user_prompt, 60000 - agent._count_tokens(sys_prompt))
current_index = len(completed_sections) + 1
pct = min(88, 15 + int((current_index / max(1, total_steps)) * 72))
self._progress(pct, f"writing:{current_index}:{total_steps}")
prefix = ""
if sec.get("section_first"):
prefix += f"# {sec_title}\n\n"
if sec.get("subsection_first") and subsection:
prefix += f"## {subsection}\n\n"
if mode == "asq" and section_id == "appendix":
# R6A-R4: Appendix is a deterministic reader-facing projection
# of the Evidence Ledger. It is never regenerated by the LLM.
en_raw = qual_engine.build_appendix_evidence_md(data_metadata)
else:
en_raw = self._stream_text(
agent.stream_deepseek(
sys_prompt, user_prompt, max_tokens=7000,
model_role="writing", thinking=False, allow_fallback=False,
),
preview_prefix=full_draft_en + prefix,
preview_stage=display_name,
)
en_raw = re.sub(r"<think>.*?</think>", "", en_raw, flags=re.S).strip()
en_raw = re.sub(rf"(?im)^#\s*{re.escape(sec_title)}\s*\n+", "", en_raw).strip()
if subsection:
en_raw = re.sub(rf"(?im)^##\s*{re.escape(subsection)}\s*\n+", "", en_raw).strip()
en_raw = re.sub(r"(?m)^(#{2,3})\s+[\d\.\s一二三四五六七八九十、]+\s*(.*)", r"\1 \2", en_raw)
en_raw = re.sub(r"(?m)^#{4,}\s+(.*)", r"**\1**", en_raw)
en_raw = re.sub(r"(?m)^#\s+(.+)$", r"## \1", en_raw)
en_raw = agent.critique_and_repair_section(
en_raw, section_id=section_id, section_name=display_name, mode=mode,
evidence_contract=evidence_packet,
)
en_raw = agent.polish_academic_section(en_raw, display_name, abstract=False, mode=mode)
if mode == "asq":
# Model prose has no authority over quote presence/count.
en_raw = self._strip_model_controlled_qualitative_quotes(en_raw, data_metadata)
allow_deterministic_citation_reinforcement = not (
mode == "asq" and section_id in {"methods", "discussion"}
)
if not is_quarantined and assigned_tag_list and allow_deterministic_citation_reinforcement:
en_raw = agent.reinforce_claim_specific_citation_coverage(
en_raw, assigned_tag_list, section_name=display_name
)
self._progress(pct, f"citation_audit:{current_index}:{total_steps}")
used_before = set(agent.used_citations)
en_resolved = agent.audit_and_resolve_citations(
prefix + en_raw, section_name=display_name, quarantine=is_quarantined
)
used_after = set(agent.used_citations)
survived = used_after - used_before
if assigned_tag_list and not is_quarantined:
required_survival = min(
len(assigned_tag_list),
max(1, int(math.ceil(len(assigned_tag_list) * CITATION_SEMANTIC_SURVIVAL_RATIO))),
)
if len(survived) < required_survival:
missing_items = [
item for item in planned_items
if str(item.get("tag") or "") not in used_after
]
needed = max(0, required_survival - len(survived))
recovery_items = agent.select_contextual_citation_recovery_items(
missing_items, section_name=display_name, section_context=en_resolved,
needed=needed, excluded_tags=used_after,
)
if len(recovery_items) < needed:
recovery_items.extend(
agent.fetch_contextual_jit_recovery_items(
topic=topic, section_name=display_name, section_context=en_resolved,
needed=needed - len(recovery_items),
excluded_tags=used_after | {str(item.get("tag") or "") for item in recovery_items},
)
)
planned_tags_for_unit = {str(item.get("tag") or "") for item in planned_items}
for recovery_item in recovery_items[:needed]:
tag = str(recovery_item.get("tag") or "")
if tag and tag not in planned_tags_for_unit:
planned_items.append(recovery_item)
planned_tags_for_unit.add(tag)
citation_plan[display_name] = planned_items
# R14-TQ2-PQ1-R1: recovery may attach evidence only to
# an already-written claim. It is never authorized to create a
# supplemental paragraph from a source merely to satisfy citation
# survival. If no existing claim can be validated, leave prose
# unchanged. Evidence coverage may later be reported as limited,
# while citation correctness itself remains fail-closed.
self._progress(pct, f"citation_recovery:{current_index}:{total_steps}")
recovered = agent.recover_citations_on_existing_claims(
en_resolved,
recovery_items[:needed],
section_name=display_name,
needed=needed,
)
if recovered != en_resolved:
en_resolved = recovered
used_after = set(agent.used_citations)
survived = used_after - used_before
if mode == "asq" and section_id == "findings":
en_resolved = self._render_planned_qualitative_quotes(
en_resolved,
task=sec,
data_metadata=data_metadata,
quote_invocation_plan=quote_invocation_plan,
)
# P0-R1.1-R2: every mutating deterministic operation must complete
# before the Evidence Fact contract seals the English unit.
en_resolved, enumeration_rows = agent.repair_and_validate_enumeration_contract(
en_resolved, section_name=display_name
)
if enumeration_rows:
logging.warning(
"Structural enumeration repaired | unit=%s count=%s details=%s",
display_name, len(enumeration_rows), enumeration_rows[:3],
)
quality_diagnostics.append({
"scope":"structural_enumeration", "name":display_name,
"status":"deterministic_repair", "count":len(enumeration_rows),
"examples":enumeration_rows[:3],
})
en_resolved, duplicate_rows = self._remove_severe_cross_unit_duplicates(en_resolved, full_draft_en)
if duplicate_rows:
logging.warning("Severe cross-unit redundancy removed | unit=%s count=%s max_similarity=%s", display_name, len(duplicate_rows), max(row["similarity"] for row in duplicate_rows))
quality_diagnostics.append({"scope":"manuscript_redundancy","name":display_name,"status":"bounded_duplicate_removed","count":len(duplicate_rows),"examples":duplicate_rows[:3]})
en_resolved, sentence_echo_rows = agent.remove_severe_cross_unit_sentence_echoes(
en_resolved, full_draft_en
)
if sentence_echo_rows:
logging.warning(
"Severe cross-unit sentence echo removed | unit=%s count=%s max_similarity=%s",
display_name, len(sentence_echo_rows),
max(row["similarity"] for row in sentence_echo_rows),
)
quality_diagnostics.append({
"scope":"manuscript_sentence_echo", "name":display_name,
"status":"bounded_duplicate_removed", "count":len(sentence_echo_rows),
"examples":sentence_echo_rows[:3],
})
# Evidence Fact repair/validation is intentionally LAST. Once this
# passes, no later operation may mutate this English unit.
if mode == "asq":
en_resolved, study_fact_rows = agent.repair_qualitative_study_fact_contract(
en_resolved, data_metadata, section_name=display_name
)
if study_fact_rows:
logging.warning(
"Qualitative Study Evidence Ledger repair | unit=%s count=%s types=%s",
display_name, len(study_fact_rows),
sorted({str(row.get("type") or "") for row in study_fact_rows}),
)
quality_diagnostics.append({
"scope":"qualitative_study_fact_integrity", "name":display_name,
"status":"deterministic_repair", "count":len(study_fact_rows),
"examples":study_fact_rows[:3],
})
agent.validate_qualitative_study_fact_contract(
en_resolved, data_metadata, section_name=display_name
)
elif mode == "amj":
en_resolved, study_fact_rows = quant_engine.repair_quantitative_study_fact_contract(
en_resolved, data_metadata, section_name=display_name
)
if study_fact_rows:
logging.warning(
"Quantitative Evidence Ledger repair | unit=%s count=%s types=%s",
display_name, len(study_fact_rows),
sorted({str(row.get("type") or "") for row in study_fact_rows}),
)
quality_diagnostics.append({
"scope":"quantitative_study_fact_integrity", "name":display_name,
"status":"deterministic_repair", "count":len(study_fact_rows),
"examples":study_fact_rows[:3],
})
quant_engine.validate_quantitative_study_fact_contract(
en_resolved, data_metadata, section_name=display_name
)
# Recompute citation survival/claim demand from the exact text that
# will be sealed, not from an earlier pre-cleanup intermediate.
if not is_quarantined and section_id in self._citation_eligible_section_ids(mode):
final_survived = set()
for tag in set(survived or set()):
marker = str((agent.citation_map.get(tag) or {}).get("in_text") or "").strip()
if marker and marker in en_resolved:
final_survived.add(tag)
survived = final_survived
citation_required_claim_counts_by_unit[display_name] = agent.count_external_literature_claims(
en_resolved, section_name=display_name
)
if assigned_tag_list:
citation_validated_tags_by_unit[display_name] = sorted(
survived, key=agent._citation_tag_number
)
logging.info(
"Top-level citation accumulation: unit=%s section=%s validated=%s external_claims=%s",
display_name, citation_unit_section_map.get(display_name, section_id),
len(survived), citation_required_claim_counts_by_unit[display_name],
)
full_draft_en += en_resolved + "\n\n"
if assigned_tag_list:
logging.info(
"Citation-first survival: section=%s planned=%s validated_new=%s",
display_name, len(assigned_tag_list), len(survived),
)
self._publish_preview(full_draft_en, display_name, force=True)
# Only sealed/reconciled prose may feed subsequent manuscript memory.
agent._extract_memory_from_text(en_resolved, sec_title)
context_tail = " ".join(en_resolved.split()[-200:])
actual_words = self._english_count_for_target(en_resolved)
if not bool(sec.get("exclude_from_word_budget")):
accumulated_deviation += actual_words - adjusted_target
self._upsert_quality_diagnostic(
quality_diagnostics, self._section_word_diagnostic(display_name, actual_words, adjusted_target)
)
completed_sections.append(display_name)
section_key = str(sec.get('section_id') or sec_title)
if self._section_is_complete(adaptive_structure, completed_sections, section_key):
frozen_section = self._extract_top_level_section(full_draft_en, sec_title)
if not frozen_section:
raise RuntimeError(f"english_section_seal_source_missing:{sec_title}")
section_integrity_seals[section_key] = self._build_section_integrity_seal(
section_key, sec_title, frozen_section
)
self._enqueue_translation_unit(
translation_agent,
unit_key=section_key, unit_name=sec_title, source_text=frozen_section,
translation_sections=translation_sections,
english_commit_text=full_draft_en,
)
# Collect only already-finished target jobs. This call never waits,
# so English Section N+1 can start while target Section N runs.
translation_sections, translation_glossary = self._collect_translation_results(
translation_sections, translation_glossary, block=False
)
partial_translation = self._assemble_partial_translation(translation_sections, adaptive_structure, include_front=True)
state.update(
{
"completed_sections": completed_sections,
"full_draft_en": full_draft_en,
"full_draft_translated": partial_translation,
"translation_sections": translation_sections,
"translation_glossary": translation_glossary,
"context_tail": context_tail,
"accumulated_deviation": accumulated_deviation,
"quality_diagnostics": quality_diagnostics,
"citation_plan": citation_plan,
"citation_minimums": citation_minimums,
"citation_reserve_targets": citation_reserve_targets,
"citation_top_level_minimums": citation_top_level_minimums,
"citation_top_level_reserve_targets": citation_top_level_reserve_targets,
"citation_unit_section_map": citation_unit_section_map,
"citation_required_claim_counts_by_unit": citation_required_claim_counts_by_unit,
"citation_validated_tags_by_unit": citation_validated_tags_by_unit,
"section_integrity_seals": section_integrity_seals,
"english_section_seal_version": ENGLISH_SECTION_SEAL_VERSION,
"agent_state": agent.export_state(),
"data_metadata": data_metadata,
}
)
self._checkpoint(
f"section_{len(completed_sections):03d}", state, pct,
display_node=f"writing_complete:{len(completed_sections)}:{total_steps}",
)
if not state.get("english_finalized"):
self._progress(91, "english_validation")
full_draft_en = _clean_trailing_references(full_draft_en, ["Reference", "References", "Reference List"])
section_order_for_fact = self._unique_top_level_sections(adaptive_structure)
if not section_order_for_fact:
raise RuntimeError("final_study_fact_section_plan_missing")
# Final English validation is validation-only. Every mutable repair
# was completed before the top-level section received its integrity
# seal; finalization must never repair the whole manuscript again.
for section_key, section_title in section_order_for_fact:
sealed_section = self._extract_top_level_section(full_draft_en, section_title)
if not sealed_section:
raise RuntimeError(f"final_english_section_missing:{section_title}")
seal = dict(section_integrity_seals.get(section_key) or {})
self._validate_section_integrity_seal(seal, sealed_section, section_title)
try:
if mode == "asq":
agent.validate_qualitative_study_fact_contract(
sealed_section, data_metadata, section_name=section_title
)
elif mode == "amj":
quant_engine.validate_quantitative_study_fact_contract(
sealed_section, data_metadata, section_name=section_title
)
except Exception as exc:
prefix = (
"final_qualitative_study_fact_contract_failed"
if mode == "asq" else "final_quantitative_study_fact_contract_failed"
)
raise RuntimeError(f"{prefix}:{section_title}") from exc
enum_checked, _enum_rows = agent.repair_and_validate_enumeration_contract(
sealed_section, section_name=f"Final sealed section: {section_title}"
)
if enum_checked.strip() != sealed_section.strip():
raise RuntimeError(f"final_enumeration_contract_failed:{section_title}")
# Validate the completed body while the Working Abstract is still in
# place. Final front matter must not be generated until all body
# sections, citation cleanup, and any Discussion rebalance are frozen.
agent.validate_manuscript_structure(full_draft_en, adaptive_structure, translated=False)
if mode == "asq":
self._validate_qualitative_quote_rendering(
full_draft_en, quote_invocation_plan, data_metadata
)
repeated_quotes = agent.find_repeated_qualitative_quotes(full_draft_en, data_metadata)
if repeated_quotes:
raise RuntimeError(f"Repeated qualitative quotations: {repeated_quotes[:5]}")
# R4-R3: the body is already citation-resolved at this point. Never
# rescan it for raw CITE tags; that destroys a valid citation ledger.
final_citation_ledger = agent.finalize_resolved_citation_ledger(full_draft_en)
references = agent.build_final_reference_lines(final_citation_ledger)
if not references:
self._record_evidence_coverage_event(
state, [("final validated references", 0, 1)],
phase="final_validated_references_empty", preview_status="limited",
)
logging.warning(
"Final validated reference list is empty; completing in evidence-limited mode without unrelated or invented references."
)
validated_by_top_level = {}
claims_by_top_level = {}
for unit_name, section_id in citation_unit_section_map.items():
if section_id not in citation_top_level_minimums:
continue
validated_by_top_level.setdefault(section_id, set()).update(
citation_validated_tags_by_unit.get(unit_name) or []
)
claims_by_top_level[section_id] = claims_by_top_level.get(section_id, 0) + int(
citation_required_claim_counts_by_unit.get(unit_name, 0) or 0
)
method_section_id = 'methods' if mode == 'asq' else 'methodology'
method_validated_tags = set(validated_by_top_level.get(method_section_id) or []) & set(final_citation_ledger)
method_authority_tags = {
tag for tag in method_validated_tags
if 'method_authority' in set((agent.citation_map.get(tag) or {}).get('evidence_lanes') or [])
}
method_external_claims = max(0, int(claims_by_top_level.get(method_section_id, 0) or 0))
final_shortfalls = []
for section_id, planned_minimum in citation_top_level_minimums.items():
validated_tags = set(validated_by_top_level.get(section_id) or [])
external_claims = max(0, int(claims_by_top_level.get(section_id, 0)))
# Evidence authority is need-based, not a word-count quota. The
# top-level planning minimum is therefore capped by the number of
# externally-oriented claims that actually survived drafting.
required = min(int(planned_minimum), external_claims) if external_claims else 0
if len(validated_tags) < required:
final_shortfalls.append((section_id, len(validated_tags), required))
final_reference_count = len(references)
if final_shortfalls or final_reference_count == 0:
if final_shortfalls:
self._record_evidence_coverage_event(
state, final_shortfalls, phase="final_claim_evidence_gate",
preview_status="limited",
)
evidence_status = "limited"
logging.warning(
"Top-level claim-evidence final Gate completed with evidence warning: validated_references=%s sections=%s shortfalls=%s",
final_reference_count, len(citation_top_level_minimums), final_shortfalls,
)
else:
evidence_status = "adequate"
self._preview_evidence_status = "adequate"
self._write_preview_payload(force=True)
logging.info(
"Top-level claim-evidence final Gate passed: validated_references=%s sections=%s",
final_reference_count, len(citation_top_level_minimums),
)
evidence_coverage_summary = self._evidence_coverage_summary(
status=evidence_status,
early_risk=bool(state.get("evidence_risk_detected")),
final_reference_count=final_reference_count,
minimums=citation_top_level_minimums,
validated_by_top_level=validated_by_top_level,
claims_by_top_level=claims_by_top_level,
events=state.get("evidence_coverage_events") or self._evidence_coverage_events,
method_authority_count=len(method_authority_tags),
method_external_claims=method_external_claims,
)
state["evidence_status"] = evidence_status
state["evidence_coverage_summary"] = evidence_coverage_summary
full_draft_en = full_draft_en.rstrip() + "\n\n# References\n" + "\n\n".join(references)
actual_words = self._english_count_for_target(full_draft_en)
self._upsert_quality_diagnostic(
quality_diagnostics, self._validate_target_length(full_draft_en, target_words)
)
body_only, _reference_tail = agent.validate_reference_tail(
full_draft_en, minimum_references=final_reference_count
)
agent.validate_manuscript_structure(body_only, adaptive_structure, translated=False)
agent.enforce_resolved_claim_specific_citations(body_only)
agent.validate_final_citation_bijection(body_only, _reference_tail, final_citation_ledger)
# Body is now frozen. Generate both Final Abstract and Final Keywords
# from the completed manuscript/evidence, then replace the provisional
# front matter deterministically. No body rebalance may occur afterward.
self._progress(92, "final_front_matter")
final_front = agent.generate_final_front_matter(
topic, keywords, body_only, data_evidence=user_data_md, mode=mode
)
final_abstract = str(final_front.get("abstract") or "").strip()
final_keywords = list(final_front.get("keywords") or [])
if not final_abstract or not final_keywords:
raise RuntimeError("final_front_matter_missing")
final_abstract, final_front_fact_rows = self._repair_validate_study_fact_contract(
mode, agent, quant_engine, final_abstract, data_metadata,
section_name="Final Abstract",
)
if not final_abstract:
raise RuntimeError("final_abstract_empty_after_study_fact_contract")
if final_front_fact_rows:
logging.warning(
"Final Abstract study-fact repair | mode=%s count=%s types=%s",
mode, len(final_front_fact_rows),
sorted({str(row.get("type") or "") for row in final_front_fact_rows}),
)
quality_diagnostics.append({
"scope":"final_abstract_study_fact_integrity", "name":"Final Abstract",
"status":"deterministic_repair", "count":len(final_front_fact_rows),
"examples":final_front_fact_rows[:3],
})
full_draft_en = replace_abstract(full_draft_en, final_abstract)
full_draft_en = self._replace_keywords(full_draft_en, final_keywords)
body_only, _reference_tail = agent.validate_reference_tail(
full_draft_en, minimum_references=final_reference_count
)
agent.validate_manuscript_structure(body_only, adaptive_structure, translated=False)
agent.enforce_resolved_claim_specific_citations(body_only)
post_front_ledger = agent.finalize_resolved_citation_ledger(body_only)
if set(post_front_ledger) != set(final_citation_ledger):
raise RuntimeError("final_front_matter_changed_citation_ledger")
agent.validate_final_citation_bijection(body_only, _reference_tail, final_citation_ledger)
self._upsert_quality_diagnostic(
quality_diagnostics, self._validate_target_length(full_draft_en, target_words)
)
# Final front matter has a different source hash from the Working
# front matter. Retranslate only __front_matter__; unchanged body
# translations are retained and reused.
section_order_for_front = self._unique_top_level_sections(adaptive_structure)
if section_order_for_front:
final_front_source = self._extract_front_matter(body_only, section_order_for_front[0][1])
self._enqueue_translation_unit(
translation_agent,
unit_key='__front_matter__', unit_name='Final Front Matter',
source_text=final_front_source,
translation_sections=translation_sections,
english_commit_text=full_draft_en,
)
translation_sections, translation_glossary = self._collect_translation_results(
translation_sections, translation_glossary, block=False
)
partial_target = self._assemble_partial_translation(
translation_sections, adaptive_structure, include_front=True
)
self._publish_preview(full_draft_en, "english_complete", force=True, complete=True)
state.update(
{
"full_draft_en": full_draft_en,
"full_draft_translated": partial_target,
"translation_sections": translation_sections,
"translation_glossary": translation_glossary,
"final_abstract": final_abstract,
"final_keywords": final_keywords,
"front_matter_phase": "final",
"final_reference_count": final_reference_count,
"final_citation_ledger": list(final_citation_ledger),
"final_citation_ledger_version": FINAL_CITATION_LEDGER_VERSION,
"evidence_status": str(state.get("evidence_status") or "adequate"),
"evidence_risk_detected": bool(state.get("evidence_risk_detected")),
"evidence_coverage_events": list(state.get("evidence_coverage_events") or []),
"evidence_coverage_summary": dict(state.get("evidence_coverage_summary") or {}),
"citation_plan": citation_plan,
"citation_minimums": citation_minimums,
"citation_reserve_targets": citation_reserve_targets,
"citation_top_level_minimums": citation_top_level_minimums,
"citation_top_level_reserve_targets": citation_top_level_reserve_targets,
"citation_unit_section_map": citation_unit_section_map,
"citation_required_claim_counts_by_unit": citation_required_claim_counts_by_unit,
"citation_validated_tags_by_unit": citation_validated_tags_by_unit,
"section_integrity_seals": section_integrity_seals,
"english_section_seal_version": ENGLISH_SECTION_SEAL_VERSION,
"quality_diagnostics": quality_diagnostics,
"english_finalized": True,
"translation_complete": False,
"agent_state": agent.export_state(),
}
)
self._checkpoint("english_complete", state, 93)
else:
full_draft_en = str(state.get("full_draft_en") or "")
full_draft_translated = str(state.get("full_draft_translated") or "")
translation_sections = dict(state.get('translation_sections') or translation_sections or {})
translation_glossary = dict(state.get('translation_glossary') or translation_glossary or {})
if not state.get("translation_complete") or not full_draft_translated:
self._progress(94, "translation")
# English is fully frozen. Wait for all previously queued section
# translations before the strict final gate; checkpoint ownership
# remains on this main execution thread.
translation_sections, translation_glossary = self._collect_translation_results(
translation_sections, translation_glossary, block=True
)
self._shutdown_translation_worker(wait=True)
body_only, reference_tail = agent.validate_reference_tail(
full_draft_en, minimum_references=max(0, int(state.get("final_reference_count") or 0))
)
section_order = self._unique_top_level_sections(adaptive_structure)
if not section_order:
raise RuntimeError('translation_section_plan_missing')
front_matter = self._extract_front_matter(body_only, section_order[0][1])
final_units = [('__front_matter__', 'Front Matter', front_matter)]
for key, title in section_order:
final_units.append((key, title, self._extract_top_level_section(body_only, title)))
for index, (unit_key, unit_name, source_text) in enumerate(final_units, start=1):
translation_sections, translation_glossary = self._translate_frozen_unit(
translation_agent, unit_key=unit_key, unit_name=unit_name, source_text=source_text,
target_lang=target_lang, translation_sections=translation_sections,
translation_glossary=translation_glossary, adaptive_structure=adaptive_structure,
include_front=True, defer_failures=False,
english_commit_text=full_draft_en, final_gate=True,
)
partial = self._assemble_partial_translation(
translation_sections, adaptive_structure, include_front=True
)
state.update({
'full_draft_en': full_draft_en,
'full_draft_translated': partial,
'translation_sections': translation_sections,
'translation_glossary': translation_glossary,
'agent_state': agent.export_state(),
})
pct = min(96, 94 + int(index * 2 / max(1, len(final_units))))
current_translation = dict(translation_sections.get(unit_key) or {})
blocking = (
current_translation.get('qa_status') not in {'passed', 'passed_with_warning'}
or not current_translation.get('translated_text')
)
if blocking:
state['translation_blocking_unit'] = str(unit_key)
state['translation_blocking_unit_name'] = str(unit_name)
state['translation_blocking_error_code'] = str(current_translation.get('error_code') or 'TRANSLATION_PENDING')
self._checkpoint(
f'translation_{unit_key}_repair_pending', state, pct,
display_node=f'translation_repair_pending:{index}:{len(final_units)}',
)
# Stop at the first blocking unit. The durable claim state is
# now checkpointed, so resume continues exactly here instead
# of spending tokens on later units or repeating prior repair.
raise RuntimeError('translation_final_gate_unresolved:' + str(unit_key))
state.pop('translation_blocking_unit', None)
state.pop('translation_blocking_unit_name', None)
state.pop('translation_blocking_error_code', None)
self._checkpoint(
f'translation_{unit_key}', state, pct,
display_node=f'translation_final_gate:{index}:{len(final_units)}',
)
unresolved_units = [
key for key, _name, _source in final_units
if (translation_sections.get(key) or {}).get('qa_status') not in {'passed', 'passed_with_warning'}
or not (translation_sections.get(key) or {}).get('translated_text')
]
if unresolved_units:
raise RuntimeError('translation_final_gate_unresolved:' + ','.join(unresolved_units[:8]))
full_draft_translated = self._assemble_partial_translation(
translation_sections, adaptive_structure, include_front=True
).rstrip()
full_draft_translated += (
f"\n\n# {agent._localized_reference_heading(target_lang)}\n" + reference_tail.strip()
)
full_draft_translated = _normalize_target_manuscript_typography(
full_draft_translated, target_lang
)
agent.validate_translation_alignment(full_draft_en, full_draft_translated)
agent.validate_reference_preservation(full_draft_en, full_draft_translated, target_lang)
self._publish_target_preview(
full_draft_translated, 'translation_complete', force=True, complete=True
)
state.update(
{
"full_draft_translated": full_draft_translated,
"translation_sections": translation_sections,
"translation_glossary": translation_glossary,
"translation_complete": True,
"agent_state": agent.export_state(),
}
)
self._checkpoint("translation_complete", state, 96)
normalized_target = _normalize_target_manuscript_typography(
full_draft_translated, target_lang
)
if normalized_target != full_draft_translated:
full_draft_translated = normalized_target
state["full_draft_translated"] = full_draft_translated
existing_output = str(state.get("output_filename") or "").strip()
if state.get("package_validated") and existing_output:
existing_path = self.output_dir / os.path.basename(existing_output)
if existing_path.is_file() and existing_path.stat().st_size > 0:
self._progress(99, "package_validated")
result = {"output_filename": existing_path.name, "output_path": str(existing_path)}
if str(state.get("evidence_status") or "") == "limited":
result.update({
"quality_status": "evidence_limited",
"safe_error_code": "W_EVIDENCE_COVERAGE_LIMITED",
"safe_error_detail": self._evidence_warning_detail(),
})
return result
self._progress(97, "packaging")
bj_time = datetime.now(timezone(timedelta(hours=8))).strftime("%Y%m%d_%H%M")
package = build_validated_package(
english_markdown=full_draft_en,
translated_markdown=full_draft_translated,
english_filename=f"Research_EN_{bj_time}.docx",
translated_filename=f"Research_{output_suffix}_{bj_time}.docx",
data_metadata=data_metadata,
mode=mode,
citation_audit=agent.citation_audit_log,
literature_metrics=agent.get_source_metrics(),
quality_diagnostics=quality_diagnostics,
evidence_coverage=state.get("evidence_coverage_summary") or {
"status": str(state.get("evidence_status") or "adequate"),
"early_risk_detected": bool(state.get("evidence_risk_detected")),
"final_reference_count": int(state.get("final_reference_count") or 0),
"sections": [],
"events": list(state.get("evidence_coverage_events") or []),
},
)
self._progress(98, "package_validation")
output_filename = f"SmartPaper_{bj_time}.zip"
pending = self.output_dir / (".pending_" + output_filename)
pending.write_bytes(package.zip_bytes)
final = self.output_dir / output_filename
os.replace(pending, final)
state.update(
{
"output_filename": output_filename,
"package_validated": True,
"evidence_status": str(state.get("evidence_status") or "adequate"),
"evidence_coverage_summary": dict(state.get("evidence_coverage_summary") or {}),
"agent_state": agent.export_state(),
}
)
self._checkpoint("package_validated", state, 99, display_node="package_validated")
result = {"output_filename": output_filename, "output_path": str(final)}
if str(state.get("evidence_status") or "") == "limited":
result.update({
"quality_status": "evidence_limited",
"safe_error_code": "W_EVIDENCE_COVERAGE_LIMITED",
"safe_error_detail": self._evidence_warning_detail(),
})
return result
def run(self) -> dict[str, Any]:
try:
return self._run_impl()
finally:
self._shutdown_translation_worker(wait=False)
def _progress_activity(self) -> None:
# Activity remains Executor-owned. We also snapshot actual request
# counters so the next Executor heartbeat can persist live UI metrics.
ds_calls, s2_calls, oa_calls = self._compute_request_counts()
try:
self.progress_callback(
activity_only=True,
ds_calls=ds_calls,
s2_calls=s2_calls,
oa_calls=oa_calls,
)
except TypeError:
pass