Spaces:
Running
Running
L2 front matter language authority and delivery-first hardening
#131
by GuXiaopeng - opened
- core/generation_runner.py +215 -44
- core/pipeline_agent.py +137 -14
- core/production_guard.py +84 -38
core/generation_runner.py
CHANGED
|
@@ -1810,6 +1810,62 @@ class GenerationRunner:
|
|
| 1810 |
|
| 1811 |
return rows
|
| 1812 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1813 |
@staticmethod
|
| 1814 |
def _section_retry_feedback(violations, *, limit=8) -> str:
|
| 1815 |
rows = []
|
|
@@ -1898,10 +1954,11 @@ class GenerationRunner:
|
|
| 1898 |
))
|
| 1899 |
return "FATAL" if core_citation_section else "ADVISORY"
|
| 1900 |
|
| 1901 |
-
#
|
| 1902 |
-
#
|
| 1903 |
-
#
|
| 1904 |
-
|
|
|
|
| 1905 |
|
| 1906 |
@staticmethod
|
| 1907 |
def _section_violation_lineage(row) -> str:
|
|
@@ -2687,6 +2744,73 @@ class GenerationRunner:
|
|
| 2687 |
"""
|
| 2688 |
return str(topic or "").strip(), str(keywords or "").strip()
|
| 2689 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2690 |
@staticmethod
|
| 2691 |
def _resolved_front_matter_keyword_policy(data_metadata: dict):
|
| 2692 |
"""Resolve one policy object for Working/Final/retry/resume/fallback paths."""
|
|
@@ -2698,8 +2822,12 @@ class GenerationRunner:
|
|
| 2698 |
|
| 2699 |
@staticmethod
|
| 2700 |
def _front_matter_keyword_fallback_terms(data_metadata: dict):
|
| 2701 |
-
"""Derive recoverable
|
| 2702 |
meta = dict(data_metadata or {})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2703 |
contract = dict(meta.get("study_design_contract") or {})
|
| 2704 |
values = []
|
| 2705 |
quant = dict(contract.get("quantitative") or {})
|
|
@@ -2733,10 +2861,12 @@ class GenerationRunner:
|
|
| 2733 |
policy = keyword_policy_from_contract(contract)
|
| 2734 |
if str(policy.get("authority_source") or "").upper() != "USER_EXPLICIT":
|
| 2735 |
return []
|
|
|
|
|
|
|
| 2736 |
return apply_keyword_policy(
|
| 2737 |
-
|
| 2738 |
policy,
|
| 2739 |
-
authoritative=
|
| 2740 |
)
|
| 2741 |
|
| 2742 |
@staticmethod
|
|
@@ -2746,7 +2876,8 @@ class GenerationRunner:
|
|
| 2746 |
contract = dict(meta.get("study_design_contract") or meta.get("front_matter_contract") or {})
|
| 2747 |
intent = dict(contract.get("user_research_intent") or meta.get("user_research_intent") or {})
|
| 2748 |
policy = keyword_policy_from_contract(contract)
|
| 2749 |
-
|
|
|
|
| 2750 |
values = apply_keyword_policy(
|
| 2751 |
candidate_keywords,
|
| 2752 |
policy,
|
|
@@ -2798,13 +2929,26 @@ class GenerationRunner:
|
|
| 2798 |
|
| 2799 |
|
| 2800 |
@staticmethod
|
| 2801 |
-
def
|
| 2802 |
-
|
| 2803 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2804 |
return ['关键词', '关键字']
|
| 2805 |
-
if code
|
| 2806 |
return ['Ключевые слова']
|
| 2807 |
-
if code
|
| 2808 |
return ['Түйін сөздер', 'Кілт сөздер']
|
| 2809 |
return ['Keywords']
|
| 2810 |
|
|
@@ -2815,7 +2959,13 @@ class GenerationRunner:
|
|
| 2815 |
meta = dict(data_metadata or {})
|
| 2816 |
contract = dict(meta.get('study_design_contract') or {})
|
| 2817 |
intent = dict(contract.get('user_research_intent') or meta.get('user_research_intent') or {})
|
| 2818 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2819 |
mapping = dict(glossary or {})
|
| 2820 |
diagnostics = []
|
| 2821 |
|
|
@@ -2847,7 +2997,7 @@ class GenerationRunner:
|
|
| 2847 |
values = [str(row.get('label') or '') for row in construct_rows if role in (row.get('roles') or []) and row.get('label')]
|
| 2848 |
if len(values) == 1:
|
| 2849 |
role_label[role] = re.sub(r'\s+', ' ', str(mapping.get(values[0]) or values[0])).strip()
|
| 2850 |
-
code =
|
| 2851 |
suffixes = []
|
| 2852 |
if 'moderation' in missing:
|
| 2853 |
label = role_label.get('moderator') or ''
|
|
@@ -2958,7 +3108,8 @@ class GenerationRunner:
|
|
| 2958 |
# R7-R2C-R5: user keywords are research-intent identities, not expendable
|
| 2959 |
# planning seeds. Freeze them for multilingual inheritance while still
|
| 2960 |
# distinguishing them from empirical construct roles.
|
| 2961 |
-
|
|
|
|
| 2962 |
if not intent_keywords:
|
| 2963 |
intent_keywords = parse_keyword_terms(keywords, explicit_user=True)
|
| 2964 |
for seed in intent_keywords:
|
|
@@ -4329,6 +4480,15 @@ class GenerationRunner:
|
|
| 4329 |
state.pop("effective_topic", None)
|
| 4330 |
state.pop("effective_keywords", None)
|
| 4331 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4332 |
hypothesis_ledger = dict(data_metadata.get("hypothesis_ledger") or {}) if mode == "amj" else {}
|
| 4333 |
hypothesis_contract = self._hypothesis_contract_directive(hypothesis_ledger)
|
| 4334 |
if hypothesis_ledger:
|
|
@@ -4496,7 +4656,7 @@ class GenerationRunner:
|
|
| 4496 |
working_front_error = None
|
| 4497 |
try:
|
| 4498 |
working_front = agent.generate_working_front_matter(
|
| 4499 |
-
|
| 4500 |
data_evidence=planning_data_md, mode=mode,
|
| 4501 |
hypothesis_contract=hypothesis_contract,
|
| 4502 |
authoritative_keywords=self._user_authoritative_front_matter_keywords(data_metadata),
|
|
@@ -4522,7 +4682,7 @@ class GenerationRunner:
|
|
| 4522 |
)
|
| 4523 |
if not working_abstract or not working_keywords:
|
| 4524 |
fallback = self._deterministic_working_front_matter(
|
| 4525 |
-
mode,
|
| 4526 |
)
|
| 4527 |
working_abstract = str(fallback.get("abstract") or "").strip()
|
| 4528 |
working_keywords = list(fallback.get("keywords") or [])
|
|
@@ -4553,7 +4713,7 @@ class GenerationRunner:
|
|
| 4553 |
)
|
| 4554 |
try:
|
| 4555 |
working_front = agent.generate_working_front_matter(
|
| 4556 |
-
|
| 4557 |
data_evidence=planning_data_md, mode=mode, validation_feedback=feedback,
|
| 4558 |
hypothesis_contract=hypothesis_contract,
|
| 4559 |
authoritative_keywords=self._user_authoritative_front_matter_keywords(data_metadata),
|
|
@@ -4599,7 +4759,7 @@ class GenerationRunner:
|
|
| 4599 |
})
|
| 4600 |
if not working_abstract or not working_keywords:
|
| 4601 |
fallback = self._deterministic_working_front_matter(
|
| 4602 |
-
mode,
|
| 4603 |
)
|
| 4604 |
working_abstract = str(fallback.get("abstract") or "").strip()
|
| 4605 |
working_keywords = list(fallback.get("keywords") or [])
|
|
@@ -4620,7 +4780,7 @@ class GenerationRunner:
|
|
| 4620 |
plan = agent.generate_outline(topic, keywords, mode=mode, data_metadata=data_metadata)
|
| 4621 |
# User title is reader-facing authority. Outline planning may adapt
|
| 4622 |
# structure but may not silently rename the study.
|
| 4623 |
-
en_topic = str(source_topic or topic).strip()
|
| 4624 |
structure = plan.get("structure", [])
|
| 4625 |
full_draft_en = (
|
| 4626 |
f"# {en_topic}\n\n## Abstract\n{working_abstract}\n\n"
|
|
@@ -4815,7 +4975,7 @@ class GenerationRunner:
|
|
| 4815 |
else:
|
| 4816 |
full_draft_en = str(state.get("full_draft_en") or "")
|
| 4817 |
if str(data_metadata.get("source_type") or "") == "system_generated" and mode == "amj":
|
| 4818 |
-
full_draft_en = self._replace_title(full_draft_en,
|
| 4819 |
canonical_resume_keywords = self._canonical_quantitative_front_matter_keywords(
|
| 4820 |
mode, [], data_metadata
|
| 4821 |
)
|
|
@@ -4934,7 +5094,7 @@ class GenerationRunner:
|
|
| 4934 |
feedback = self._section_retry_feedback(resumed_front_rows)
|
| 4935 |
try:
|
| 4936 |
working_front = agent.generate_working_front_matter(
|
| 4937 |
-
|
| 4938 |
data_evidence=planning_data_md, mode=mode, validation_feedback=feedback,
|
| 4939 |
hypothesis_contract=hypothesis_contract,
|
| 4940 |
authoritative_keywords=self._user_authoritative_front_matter_keywords(data_metadata),
|
|
@@ -4957,13 +5117,13 @@ class GenerationRunner:
|
|
| 4957 |
type(exc).__name__,
|
| 4958 |
)
|
| 4959 |
fallback = self._deterministic_working_front_matter(
|
| 4960 |
-
mode,
|
| 4961 |
)
|
| 4962 |
candidate_abstract = str(fallback.get("abstract") or "").strip()
|
| 4963 |
candidate_keywords = list(fallback.get("keywords") or [])
|
| 4964 |
if not candidate_abstract or not candidate_keywords:
|
| 4965 |
fallback = self._deterministic_working_front_matter(
|
| 4966 |
-
mode,
|
| 4967 |
)
|
| 4968 |
candidate_abstract = str(fallback.get("abstract") or "").strip()
|
| 4969 |
candidate_keywords = list(fallback.get("keywords") or [])
|
|
@@ -4985,7 +5145,7 @@ class GenerationRunner:
|
|
| 4985 |
logging.warning("Adaptive resumed Working Abstract accepted with residual diagnostics | count=%s", len(residual_front_rows))
|
| 4986 |
if not candidate_abstract:
|
| 4987 |
fallback = self._deterministic_working_front_matter(
|
| 4988 |
-
mode,
|
| 4989 |
)
|
| 4990 |
candidate_abstract = str(fallback.get("abstract") or "").strip()
|
| 4991 |
candidate_keywords = list(fallback.get("keywords") or candidate_keywords)
|
|
@@ -5188,7 +5348,7 @@ class GenerationRunner:
|
|
| 5188 |
if not working_abstract and not state.get("english_finalized"):
|
| 5189 |
try:
|
| 5190 |
working_front = agent.generate_working_front_matter(
|
| 5191 |
-
|
| 5192 |
data_evidence=planning_data_md, mode=mode,
|
| 5193 |
hypothesis_contract=hypothesis_contract,
|
| 5194 |
authoritative_keywords=self._user_authoritative_front_matter_keywords(data_metadata),
|
|
@@ -5203,7 +5363,7 @@ class GenerationRunner:
|
|
| 5203 |
type(exc).__name__,
|
| 5204 |
)
|
| 5205 |
working_front = self._deterministic_working_front_matter(
|
| 5206 |
-
mode,
|
| 5207 |
)
|
| 5208 |
quality_diagnostics.append({
|
| 5209 |
"scope": "delivery_first_working_front_matter_fallback",
|
|
@@ -5221,7 +5381,7 @@ class GenerationRunner:
|
|
| 5221 |
)
|
| 5222 |
if not working_abstract or not working_keywords:
|
| 5223 |
working_front = self._deterministic_working_front_matter(
|
| 5224 |
-
mode,
|
| 5225 |
)
|
| 5226 |
working_abstract = str(working_front.get("abstract") or "").strip()
|
| 5227 |
working_keywords = list(working_front.get("keywords") or [])
|
|
@@ -5242,7 +5402,7 @@ class GenerationRunner:
|
|
| 5242 |
feedback = self._section_retry_feedback(front_fact_rows)
|
| 5243 |
try:
|
| 5244 |
working_front = agent.generate_working_front_matter(
|
| 5245 |
-
|
| 5246 |
data_evidence=planning_data_md, mode=mode, validation_feedback=feedback,
|
| 5247 |
hypothesis_contract=hypothesis_contract,
|
| 5248 |
authoritative_keywords=self._user_authoritative_front_matter_keywords(data_metadata),
|
|
@@ -5288,7 +5448,7 @@ class GenerationRunner:
|
|
| 5288 |
})
|
| 5289 |
if not working_abstract or not working_keywords:
|
| 5290 |
working_front = self._deterministic_working_front_matter(
|
| 5291 |
-
mode,
|
| 5292 |
)
|
| 5293 |
working_abstract = str(working_front.get("abstract") or "").strip()
|
| 5294 |
working_keywords = list(working_front.get("keywords") or [])
|
|
@@ -6440,7 +6600,7 @@ The previous attempt for this same manuscript unit was rejected by validation. R
|
|
| 6440 |
self._progress(92, "final_front_matter")
|
| 6441 |
try:
|
| 6442 |
final_front = agent.generate_final_front_matter(
|
| 6443 |
-
|
| 6444 |
hypothesis_contract=hypothesis_contract,
|
| 6445 |
authoritative_keywords=self._user_authoritative_front_matter_keywords(data_metadata),
|
| 6446 |
keyword_policy=self._resolved_front_matter_keyword_policy(data_metadata),
|
|
@@ -6452,10 +6612,10 @@ The previous attempt for this same manuscript unit was rejected by validation. R
|
|
| 6452 |
)
|
| 6453 |
final_front = {
|
| 6454 |
"abstract": self._deterministic_final_abstract(
|
| 6455 |
-
mode, source_topic or topic, data_metadata, hypothesis_ledger=hypothesis_ledger
|
| 6456 |
),
|
| 6457 |
"keywords": self._canonical_quantitative_front_matter_keywords(
|
| 6458 |
-
mode,
|
| 6459 |
) or self._front_matter_keyword_fallback_terms(data_metadata),
|
| 6460 |
}
|
| 6461 |
self._record_adaptive_completion_residual(
|
|
@@ -6474,10 +6634,10 @@ The previous attempt for this same manuscript unit was rejected by validation. R
|
|
| 6474 |
)
|
| 6475 |
if not final_abstract or not final_keywords:
|
| 6476 |
final_abstract = final_abstract or self._deterministic_final_abstract(
|
| 6477 |
-
mode, source_topic or topic, data_metadata, hypothesis_ledger=hypothesis_ledger
|
| 6478 |
)
|
| 6479 |
final_keywords = final_keywords or self._canonical_quantitative_front_matter_keywords(
|
| 6480 |
-
mode,
|
| 6481 |
) or self._front_matter_keyword_fallback_terms(data_metadata)
|
| 6482 |
self._record_adaptive_completion_residual(
|
| 6483 |
quality_notes, [{
|
|
@@ -6505,7 +6665,7 @@ The previous attempt for this same manuscript unit was rejected by validation. R
|
|
| 6505 |
)
|
| 6506 |
try:
|
| 6507 |
final_front = agent.generate_final_front_matter(
|
| 6508 |
-
|
| 6509 |
validation_feedback=feedback, hypothesis_contract=hypothesis_contract,
|
| 6510 |
authoritative_keywords=self._user_authoritative_front_matter_keywords(data_metadata),
|
| 6511 |
keyword_policy=self._resolved_front_matter_keyword_policy(data_metadata),
|
|
@@ -6517,10 +6677,10 @@ The previous attempt for this same manuscript unit was rejected by validation. R
|
|
| 6517 |
)
|
| 6518 |
final_front = {
|
| 6519 |
"abstract": self._deterministic_final_abstract(
|
| 6520 |
-
mode, source_topic or topic, data_metadata, hypothesis_ledger=hypothesis_ledger
|
| 6521 |
),
|
| 6522 |
"keywords": final_keywords or self._canonical_quantitative_front_matter_keywords(
|
| 6523 |
-
mode,
|
| 6524 |
) or self._front_matter_keyword_fallback_terms(data_metadata),
|
| 6525 |
}
|
| 6526 |
final_abstract = str(final_front.get("abstract") or "").strip()
|
|
@@ -6533,10 +6693,10 @@ The previous attempt for this same manuscript unit was rejected by validation. R
|
|
| 6533 |
)
|
| 6534 |
if not final_abstract or not final_keywords:
|
| 6535 |
final_abstract = final_abstract or self._deterministic_final_abstract(
|
| 6536 |
-
mode, source_topic or topic, data_metadata, hypothesis_ledger=hypothesis_ledger
|
| 6537 |
)
|
| 6538 |
final_keywords = final_keywords or self._canonical_quantitative_front_matter_keywords(
|
| 6539 |
-
mode,
|
| 6540 |
) or self._front_matter_keyword_fallback_terms(data_metadata)
|
| 6541 |
self._record_adaptive_completion_residual(
|
| 6542 |
quality_notes, [{
|
|
@@ -6587,7 +6747,7 @@ The previous attempt for this same manuscript unit was rejected by validation. R
|
|
| 6587 |
)
|
| 6588 |
if residual_final_rows:
|
| 6589 |
deterministic_abstract = self._deterministic_final_abstract(
|
| 6590 |
-
mode, source_topic or topic, data_metadata, hypothesis_ledger=hypothesis_ledger
|
| 6591 |
)
|
| 6592 |
hard_rows = self._study_fact_violations(
|
| 6593 |
mode, agent, quant_engine, deterministic_abstract, data_metadata, section_name="Final Abstract"
|
|
@@ -6616,12 +6776,12 @@ The previous attempt for this same manuscript unit was rejected by validation. R
|
|
| 6616 |
"examples":final_front_fact_rows[:3],
|
| 6617 |
})
|
| 6618 |
state.pop("final_abstract_retry_exhausted", None)
|
| 6619 |
-
final_title = str(source_topic or topic or "").strip()
|
| 6620 |
if mode == "amj" and str(data_metadata.get("source_type") or "") == "system_generated":
|
| 6621 |
semantic_contract = dict(data_metadata.get("front_matter_contract") or {})
|
| 6622 |
reconciled_front = agent.reconcile_quantitative_reader_front_matter(
|
| 6623 |
topic=source_topic, keywords=source_keywords, contract=semantic_contract,
|
| 6624 |
-
manuscript=body_only,
|
| 6625 |
)
|
| 6626 |
candidate_title = re.sub(r"\s+", " ", str(reconciled_front.get("title") or "")).strip()
|
| 6627 |
candidate_keywords = self._canonical_quantitative_front_matter_keywords(
|
|
@@ -6640,6 +6800,17 @@ The previous attempt for this same manuscript unit was rejected by validation. R
|
|
| 6640 |
full_draft_en = replace_abstract(full_draft_en, final_abstract)
|
| 6641 |
full_draft_en = self._replace_title(full_draft_en, final_title)
|
| 6642 |
full_draft_en = self._replace_keywords(full_draft_en, final_keywords)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6643 |
if mode == "amj" and hypothesis_ledger:
|
| 6644 |
post_front_hypothesis_rows = self._hypothesis_contract_violations(
|
| 6645 |
full_draft_en, hypothesis_ledger, section_name="Full Manuscript", require_all=True
|
|
|
|
| 1810 |
|
| 1811 |
return rows
|
| 1812 |
|
| 1813 |
+
def _delivery_first_final_abstract_language_guard(
|
| 1814 |
+
self, *, mode: str, agent, quant_engine, final_abstract: str, reader_topic_en: str,
|
| 1815 |
+
final_title: str, data_metadata: dict, hypothesis_ledger: dict | None, quality_notes: list,
|
| 1816 |
+
) -> tuple[str, bool]:
|
| 1817 |
+
"""Repair residual non-English script without turning a reader-surface defect into P0 failure.
|
| 1818 |
+
|
| 1819 |
+
Returns ``(abstract, rebuilt)``. Explicit P1 integrity validators remain
|
| 1820 |
+
authoritative; only the language-surface defect is downgraded when a
|
| 1821 |
+
provider-neutral clean reconstruction cannot be proven safe.
|
| 1822 |
+
"""
|
| 1823 |
+
current = str(final_abstract or "").strip()
|
| 1824 |
+
if not agent._english_reader_surface_has_foreign_script(current):
|
| 1825 |
+
return current, False
|
| 1826 |
+
|
| 1827 |
+
language_fallback = self._deterministic_final_abstract(
|
| 1828 |
+
mode, reader_topic_en or final_title, data_metadata, hypothesis_ledger=hypothesis_ledger
|
| 1829 |
+
)
|
| 1830 |
+
language_rows = self._study_fact_violations(
|
| 1831 |
+
mode, agent, quant_engine, language_fallback, data_metadata, section_name="Final Abstract"
|
| 1832 |
+
)
|
| 1833 |
+
if hypothesis_ledger:
|
| 1834 |
+
language_rows.extend(self._hypothesis_contract_violations(
|
| 1835 |
+
language_fallback, hypothesis_ledger, section_name="Final Abstract"
|
| 1836 |
+
))
|
| 1837 |
+
language_rows.extend(self._final_abstract_finding_violations(
|
| 1838 |
+
mode, language_fallback, data_metadata, hypothesis_ledger=hypothesis_ledger
|
| 1839 |
+
))
|
| 1840 |
+
fatal_language_rows, advisory_language_rows = self._partition_section_violations(
|
| 1841 |
+
language_rows, section_name="Final Abstract", mode=mode, exhausted=True
|
| 1842 |
+
)
|
| 1843 |
+
if (
|
| 1844 |
+
language_fallback
|
| 1845 |
+
and not agent._english_reader_surface_has_foreign_script(language_fallback)
|
| 1846 |
+
and not fatal_language_rows
|
| 1847 |
+
):
|
| 1848 |
+
self._record_adaptive_completion_residual(
|
| 1849 |
+
quality_notes,
|
| 1850 |
+
advisory_language_rows + [{
|
| 1851 |
+
"type": "final_english_front_matter_language_fallback",
|
| 1852 |
+
"reason": "Residual non-English script in the Final Abstract was replaced by a deterministic English reconstruction from frozen research authority."
|
| 1853 |
+
}],
|
| 1854 |
+
section_name="Final Abstract",
|
| 1855 |
+
stage="delivery_first_front_matter_language_fallback",
|
| 1856 |
+
)
|
| 1857 |
+
return language_fallback, True
|
| 1858 |
+
|
| 1859 |
+
self._record_adaptive_completion_residual(
|
| 1860 |
+
quality_notes, [{
|
| 1861 |
+
"type": "final_english_front_matter_language_warning",
|
| 1862 |
+
"reason": "Final Abstract still contains non-English script after deterministic language fallback; delivery was preserved under the P0 Delivery-First contract."
|
| 1863 |
+
}],
|
| 1864 |
+
section_name="Final Abstract",
|
| 1865 |
+
stage="delivery_first_front_matter_language_warning",
|
| 1866 |
+
)
|
| 1867 |
+
return current, False
|
| 1868 |
+
|
| 1869 |
@staticmethod
|
| 1870 |
def _section_retry_feedback(violations, *, limit=8) -> str:
|
| 1871 |
rows = []
|
|
|
|
| 1954 |
))
|
| 1955 |
return "FATAL" if core_citation_section else "ADVISORY"
|
| 1956 |
|
| 1957 |
+
# Delivery-First hardening: unknown/new validators are not allowed to
|
| 1958 |
+
# create an accidental terminal path merely because a developer forgot
|
| 1959 |
+
# to register their severity. Only explicit P1/hard-failure identities
|
| 1960 |
+
# above may remain fatal after bounded repair.
|
| 1961 |
+
return "ADVISORY"
|
| 1962 |
|
| 1963 |
@staticmethod
|
| 1964 |
def _section_violation_lineage(row) -> str:
|
|
|
|
| 2744 |
"""
|
| 2745 |
return str(topic or "").strip(), str(keywords or "").strip()
|
| 2746 |
|
| 2747 |
+
@staticmethod
|
| 2748 |
+
def _front_matter_language_projection_hash(topic: str, keywords) -> str:
|
| 2749 |
+
terms = parse_keyword_terms(keywords, explicit_user=True)
|
| 2750 |
+
payload = json.dumps(
|
| 2751 |
+
{"title": str(topic or "").strip(), "keywords": terms},
|
| 2752 |
+
ensure_ascii=False, sort_keys=True, separators=(",", ":"),
|
| 2753 |
+
).encode("utf-8")
|
| 2754 |
+
return hashlib.sha256(payload).hexdigest()
|
| 2755 |
+
|
| 2756 |
+
def _resolve_english_front_matter_projection(
|
| 2757 |
+
self, agent, state: dict, data_metadata: dict, source_topic: str, source_keywords
|
| 2758 |
+
) -> dict:
|
| 2759 |
+
"""Resolve/cache a language-only English projection without mutating user intent."""
|
| 2760 |
+
meta = dict(data_metadata or {})
|
| 2761 |
+
contract = dict(meta.get("study_design_contract") or meta.get("front_matter_contract") or {})
|
| 2762 |
+
intent = dict(contract.get("user_research_intent") or meta.get("user_research_intent") or {})
|
| 2763 |
+
semantic_title = str(intent.get("source_topic") or source_topic or "").strip()
|
| 2764 |
+
semantic_keywords = list(intent.get("source_keywords") or parse_keyword_terms(source_keywords, explicit_user=True))
|
| 2765 |
+
source_hash = self._front_matter_language_projection_hash(semantic_title, semantic_keywords)
|
| 2766 |
+
expected_count = len(semantic_keywords)
|
| 2767 |
+
|
| 2768 |
+
cached = dict(
|
| 2769 |
+
state.get("front_matter_language_projection")
|
| 2770 |
+
or meta.get("front_matter_language_projection")
|
| 2771 |
+
or {}
|
| 2772 |
+
)
|
| 2773 |
+
if (
|
| 2774 |
+
cached.get("version") == "front-matter-language-authority-v1"
|
| 2775 |
+
and cached.get("source_hash") == source_hash
|
| 2776 |
+
):
|
| 2777 |
+
try:
|
| 2778 |
+
validated = agent.validate_reader_facing_english_projection(
|
| 2779 |
+
cached, expected_keyword_count=expected_count
|
| 2780 |
+
)
|
| 2781 |
+
return {
|
| 2782 |
+
"version": "front-matter-language-authority-v1",
|
| 2783 |
+
"source_hash": source_hash,
|
| 2784 |
+
"english_title": validated["title"],
|
| 2785 |
+
"english_keywords": list(validated["keywords"]),
|
| 2786 |
+
"translation_applied": bool(cached.get("translation_applied")),
|
| 2787 |
+
}
|
| 2788 |
+
except Exception as exc:
|
| 2789 |
+
logging.warning(
|
| 2790 |
+
"Cached English front-matter language projection invalid; rebuilding | error=%s",
|
| 2791 |
+
type(exc).__name__,
|
| 2792 |
+
)
|
| 2793 |
+
|
| 2794 |
+
resolved = agent.normalize_reader_facing_front_matter_to_english(
|
| 2795 |
+
semantic_title, semantic_keywords
|
| 2796 |
+
)
|
| 2797 |
+
validated = agent.validate_reader_facing_english_projection(
|
| 2798 |
+
resolved, expected_keyword_count=expected_count
|
| 2799 |
+
)
|
| 2800 |
+
projection = {
|
| 2801 |
+
"version": "front-matter-language-authority-v1",
|
| 2802 |
+
"source_hash": source_hash,
|
| 2803 |
+
"english_title": validated["title"],
|
| 2804 |
+
"english_keywords": list(validated["keywords"]),
|
| 2805 |
+
"translation_applied": bool(resolved.get("translation_applied")),
|
| 2806 |
+
}
|
| 2807 |
+
# Presentation projection is additive metadata only. Original source_topic
|
| 2808 |
+
# and source_keywords inside user_research_intent remain byte-for-byte authority.
|
| 2809 |
+
data_metadata["front_matter_language_projection"] = dict(projection)
|
| 2810 |
+
state["front_matter_language_projection"] = dict(projection)
|
| 2811 |
+
state["data_metadata"] = data_metadata
|
| 2812 |
+
return projection
|
| 2813 |
+
|
| 2814 |
@staticmethod
|
| 2815 |
def _resolved_front_matter_keyword_policy(data_metadata: dict):
|
| 2816 |
"""Resolve one policy object for Working/Final/retry/resume/fallback paths."""
|
|
|
|
| 2822 |
|
| 2823 |
@staticmethod
|
| 2824 |
def _front_matter_keyword_fallback_terms(data_metadata: dict):
|
| 2825 |
+
"""Derive recoverable English reader keywords from frozen study-design authority."""
|
| 2826 |
meta = dict(data_metadata or {})
|
| 2827 |
+
projection = dict(meta.get("front_matter_language_projection") or {})
|
| 2828 |
+
projected = [str(item).strip() for item in (projection.get("english_keywords") or []) if str(item).strip()]
|
| 2829 |
+
if projected:
|
| 2830 |
+
return projected
|
| 2831 |
contract = dict(meta.get("study_design_contract") or {})
|
| 2832 |
values = []
|
| 2833 |
quant = dict(contract.get("quantitative") or {})
|
|
|
|
| 2861 |
policy = keyword_policy_from_contract(contract)
|
| 2862 |
if str(policy.get("authority_source") or "").upper() != "USER_EXPLICIT":
|
| 2863 |
return []
|
| 2864 |
+
projection = dict(meta.get("front_matter_language_projection") or {})
|
| 2865 |
+
authoritative = list(projection.get("english_keywords") or intent.get("source_keywords") or [])
|
| 2866 |
return apply_keyword_policy(
|
| 2867 |
+
authoritative,
|
| 2868 |
policy,
|
| 2869 |
+
authoritative=authoritative,
|
| 2870 |
)
|
| 2871 |
|
| 2872 |
@staticmethod
|
|
|
|
| 2876 |
contract = dict(meta.get("study_design_contract") or meta.get("front_matter_contract") or {})
|
| 2877 |
intent = dict(contract.get("user_research_intent") or meta.get("user_research_intent") or {})
|
| 2878 |
policy = keyword_policy_from_contract(contract)
|
| 2879 |
+
projection = dict(meta.get("front_matter_language_projection") or {})
|
| 2880 |
+
authoritative = list(projection.get("english_keywords") or intent.get("source_keywords") or [])
|
| 2881 |
values = apply_keyword_policy(
|
| 2882 |
candidate_keywords,
|
| 2883 |
policy,
|
|
|
|
| 2929 |
|
| 2930 |
|
| 2931 |
@staticmethod
|
| 2932 |
+
def _target_front_matter_language_code(target_lang: str) -> str:
|
| 2933 |
+
value = str(target_lang or '').strip().casefold().replace('_', '-')
|
| 2934 |
+
if value.startswith('zh') or 'chinese' in value or '中文' in value or '简体中文' in value:
|
| 2935 |
+
return 'zh'
|
| 2936 |
+
if value.startswith('ru') or 'russian' in value or 'рус' in value:
|
| 2937 |
+
return 'ru'
|
| 2938 |
+
if value.startswith('kk') or value.startswith('kz') or 'kazakh' in value or 'қазақ' in value:
|
| 2939 |
+
return 'kk'
|
| 2940 |
+
if value.startswith('en') or 'english' in value:
|
| 2941 |
+
return 'en'
|
| 2942 |
+
return value
|
| 2943 |
+
|
| 2944 |
+
@classmethod
|
| 2945 |
+
def _target_keyword_heading_aliases(cls, target_lang: str) -> list[str]:
|
| 2946 |
+
code = cls._target_front_matter_language_code(target_lang)
|
| 2947 |
+
if code == 'zh':
|
| 2948 |
return ['关键词', '关键字']
|
| 2949 |
+
if code == 'ru':
|
| 2950 |
return ['Ключевые слова']
|
| 2951 |
+
if code == 'kk':
|
| 2952 |
return ['Түйін сөздер', 'Кілт сөздер']
|
| 2953 |
return ['Keywords']
|
| 2954 |
|
|
|
|
| 2959 |
meta = dict(data_metadata or {})
|
| 2960 |
contract = dict(meta.get('study_design_contract') or {})
|
| 2961 |
intent = dict(contract.get('user_research_intent') or meta.get('user_research_intent') or {})
|
| 2962 |
+
projection = dict(meta.get('front_matter_language_projection') or {})
|
| 2963 |
+
source_keywords = list(
|
| 2964 |
+
projection.get('english_keywords')
|
| 2965 |
+
or intent.get('source_keywords')
|
| 2966 |
+
or contract.get('source_keywords')
|
| 2967 |
+
or []
|
| 2968 |
+
)
|
| 2969 |
mapping = dict(glossary or {})
|
| 2970 |
diagnostics = []
|
| 2971 |
|
|
|
|
| 2997 |
values = [str(row.get('label') or '') for row in construct_rows if role in (row.get('roles') or []) and row.get('label')]
|
| 2998 |
if len(values) == 1:
|
| 2999 |
role_label[role] = re.sub(r'\s+', ' ', str(mapping.get(values[0]) or values[0])).strip()
|
| 3000 |
+
code = cls._target_front_matter_language_code(target_lang)
|
| 3001 |
suffixes = []
|
| 3002 |
if 'moderation' in missing:
|
| 3003 |
label = role_label.get('moderator') or ''
|
|
|
|
| 3108 |
# R7-R2C-R5: user keywords are research-intent identities, not expendable
|
| 3109 |
# planning seeds. Freeze them for multilingual inheritance while still
|
| 3110 |
# distinguishing them from empirical construct roles.
|
| 3111 |
+
projection = dict(meta.get('front_matter_language_projection') or {})
|
| 3112 |
+
intent_keywords = list(projection.get('english_keywords') or user_intent.get('source_keywords') or [])
|
| 3113 |
if not intent_keywords:
|
| 3114 |
intent_keywords = parse_keyword_terms(keywords, explicit_user=True)
|
| 3115 |
for seed in intent_keywords:
|
|
|
|
| 4480 |
state.pop("effective_topic", None)
|
| 4481 |
state.pop("effective_keywords", None)
|
| 4482 |
|
| 4483 |
+
front_matter_projection = self._resolve_english_front_matter_projection(
|
| 4484 |
+
agent, state, data_metadata, source_topic, source_keywords
|
| 4485 |
+
)
|
| 4486 |
+
reader_topic_en = str(front_matter_projection.get("english_title") or source_topic or topic or "").strip()
|
| 4487 |
+
reader_keywords_en = list(front_matter_projection.get("english_keywords") or [])
|
| 4488 |
+
reader_keywords_prompt = "; ".join(reader_keywords_en)
|
| 4489 |
+
data_metadata["front_matter_language_projection"] = dict(front_matter_projection)
|
| 4490 |
+
state["data_metadata"] = data_metadata
|
| 4491 |
+
|
| 4492 |
hypothesis_ledger = dict(data_metadata.get("hypothesis_ledger") or {}) if mode == "amj" else {}
|
| 4493 |
hypothesis_contract = self._hypothesis_contract_directive(hypothesis_ledger)
|
| 4494 |
if hypothesis_ledger:
|
|
|
|
| 4656 |
working_front_error = None
|
| 4657 |
try:
|
| 4658 |
working_front = agent.generate_working_front_matter(
|
| 4659 |
+
reader_topic_en, reader_keywords_prompt, research_logic_contract, synthesis_result,
|
| 4660 |
data_evidence=planning_data_md, mode=mode,
|
| 4661 |
hypothesis_contract=hypothesis_contract,
|
| 4662 |
authoritative_keywords=self._user_authoritative_front_matter_keywords(data_metadata),
|
|
|
|
| 4682 |
)
|
| 4683 |
if not working_abstract or not working_keywords:
|
| 4684 |
fallback = self._deterministic_working_front_matter(
|
| 4685 |
+
mode, reader_topic_en, reader_keywords_prompt, data_metadata, hypothesis_ledger=hypothesis_ledger
|
| 4686 |
)
|
| 4687 |
working_abstract = str(fallback.get("abstract") or "").strip()
|
| 4688 |
working_keywords = list(fallback.get("keywords") or [])
|
|
|
|
| 4713 |
)
|
| 4714 |
try:
|
| 4715 |
working_front = agent.generate_working_front_matter(
|
| 4716 |
+
reader_topic_en, reader_keywords_prompt, research_logic_contract, synthesis_result,
|
| 4717 |
data_evidence=planning_data_md, mode=mode, validation_feedback=feedback,
|
| 4718 |
hypothesis_contract=hypothesis_contract,
|
| 4719 |
authoritative_keywords=self._user_authoritative_front_matter_keywords(data_metadata),
|
|
|
|
| 4759 |
})
|
| 4760 |
if not working_abstract or not working_keywords:
|
| 4761 |
fallback = self._deterministic_working_front_matter(
|
| 4762 |
+
mode, reader_topic_en, reader_keywords_prompt, data_metadata, hypothesis_ledger=hypothesis_ledger
|
| 4763 |
)
|
| 4764 |
working_abstract = str(fallback.get("abstract") or "").strip()
|
| 4765 |
working_keywords = list(fallback.get("keywords") or [])
|
|
|
|
| 4780 |
plan = agent.generate_outline(topic, keywords, mode=mode, data_metadata=data_metadata)
|
| 4781 |
# User title is reader-facing authority. Outline planning may adapt
|
| 4782 |
# structure but may not silently rename the study.
|
| 4783 |
+
en_topic = str(reader_topic_en or source_topic or topic).strip()
|
| 4784 |
structure = plan.get("structure", [])
|
| 4785 |
full_draft_en = (
|
| 4786 |
f"# {en_topic}\n\n## Abstract\n{working_abstract}\n\n"
|
|
|
|
| 4975 |
else:
|
| 4976 |
full_draft_en = str(state.get("full_draft_en") or "")
|
| 4977 |
if str(data_metadata.get("source_type") or "") == "system_generated" and mode == "amj":
|
| 4978 |
+
full_draft_en = self._replace_title(full_draft_en, reader_topic_en)
|
| 4979 |
canonical_resume_keywords = self._canonical_quantitative_front_matter_keywords(
|
| 4980 |
mode, [], data_metadata
|
| 4981 |
)
|
|
|
|
| 5094 |
feedback = self._section_retry_feedback(resumed_front_rows)
|
| 5095 |
try:
|
| 5096 |
working_front = agent.generate_working_front_matter(
|
| 5097 |
+
reader_topic_en, reader_keywords_prompt, research_logic_contract, synthesis_result,
|
| 5098 |
data_evidence=planning_data_md, mode=mode, validation_feedback=feedback,
|
| 5099 |
hypothesis_contract=hypothesis_contract,
|
| 5100 |
authoritative_keywords=self._user_authoritative_front_matter_keywords(data_metadata),
|
|
|
|
| 5117 |
type(exc).__name__,
|
| 5118 |
)
|
| 5119 |
fallback = self._deterministic_working_front_matter(
|
| 5120 |
+
mode, reader_topic_en, reader_keywords_prompt, data_metadata, hypothesis_ledger=hypothesis_ledger
|
| 5121 |
)
|
| 5122 |
candidate_abstract = str(fallback.get("abstract") or "").strip()
|
| 5123 |
candidate_keywords = list(fallback.get("keywords") or [])
|
| 5124 |
if not candidate_abstract or not candidate_keywords:
|
| 5125 |
fallback = self._deterministic_working_front_matter(
|
| 5126 |
+
mode, reader_topic_en, reader_keywords_prompt, data_metadata, hypothesis_ledger=hypothesis_ledger
|
| 5127 |
)
|
| 5128 |
candidate_abstract = str(fallback.get("abstract") or "").strip()
|
| 5129 |
candidate_keywords = list(fallback.get("keywords") or [])
|
|
|
|
| 5145 |
logging.warning("Adaptive resumed Working Abstract accepted with residual diagnostics | count=%s", len(residual_front_rows))
|
| 5146 |
if not candidate_abstract:
|
| 5147 |
fallback = self._deterministic_working_front_matter(
|
| 5148 |
+
mode, reader_topic_en, reader_keywords_prompt, data_metadata, hypothesis_ledger=hypothesis_ledger
|
| 5149 |
)
|
| 5150 |
candidate_abstract = str(fallback.get("abstract") or "").strip()
|
| 5151 |
candidate_keywords = list(fallback.get("keywords") or candidate_keywords)
|
|
|
|
| 5348 |
if not working_abstract and not state.get("english_finalized"):
|
| 5349 |
try:
|
| 5350 |
working_front = agent.generate_working_front_matter(
|
| 5351 |
+
reader_topic_en, reader_keywords_prompt, research_logic_contract, synthesis_result,
|
| 5352 |
data_evidence=planning_data_md, mode=mode,
|
| 5353 |
hypothesis_contract=hypothesis_contract,
|
| 5354 |
authoritative_keywords=self._user_authoritative_front_matter_keywords(data_metadata),
|
|
|
|
| 5363 |
type(exc).__name__,
|
| 5364 |
)
|
| 5365 |
working_front = self._deterministic_working_front_matter(
|
| 5366 |
+
mode, reader_topic_en, reader_keywords_prompt, data_metadata, hypothesis_ledger=hypothesis_ledger
|
| 5367 |
)
|
| 5368 |
quality_diagnostics.append({
|
| 5369 |
"scope": "delivery_first_working_front_matter_fallback",
|
|
|
|
| 5381 |
)
|
| 5382 |
if not working_abstract or not working_keywords:
|
| 5383 |
working_front = self._deterministic_working_front_matter(
|
| 5384 |
+
mode, reader_topic_en, reader_keywords_prompt, data_metadata, hypothesis_ledger=hypothesis_ledger
|
| 5385 |
)
|
| 5386 |
working_abstract = str(working_front.get("abstract") or "").strip()
|
| 5387 |
working_keywords = list(working_front.get("keywords") or [])
|
|
|
|
| 5402 |
feedback = self._section_retry_feedback(front_fact_rows)
|
| 5403 |
try:
|
| 5404 |
working_front = agent.generate_working_front_matter(
|
| 5405 |
+
reader_topic_en, reader_keywords_prompt, research_logic_contract, synthesis_result,
|
| 5406 |
data_evidence=planning_data_md, mode=mode, validation_feedback=feedback,
|
| 5407 |
hypothesis_contract=hypothesis_contract,
|
| 5408 |
authoritative_keywords=self._user_authoritative_front_matter_keywords(data_metadata),
|
|
|
|
| 5448 |
})
|
| 5449 |
if not working_abstract or not working_keywords:
|
| 5450 |
working_front = self._deterministic_working_front_matter(
|
| 5451 |
+
mode, reader_topic_en, reader_keywords_prompt, data_metadata, hypothesis_ledger=hypothesis_ledger
|
| 5452 |
)
|
| 5453 |
working_abstract = str(working_front.get("abstract") or "").strip()
|
| 5454 |
working_keywords = list(working_front.get("keywords") or [])
|
|
|
|
| 6600 |
self._progress(92, "final_front_matter")
|
| 6601 |
try:
|
| 6602 |
final_front = agent.generate_final_front_matter(
|
| 6603 |
+
reader_topic_en, reader_keywords_prompt, body_only, data_evidence=user_data_md, mode=mode,
|
| 6604 |
hypothesis_contract=hypothesis_contract,
|
| 6605 |
authoritative_keywords=self._user_authoritative_front_matter_keywords(data_metadata),
|
| 6606 |
keyword_policy=self._resolved_front_matter_keyword_policy(data_metadata),
|
|
|
|
| 6612 |
)
|
| 6613 |
final_front = {
|
| 6614 |
"abstract": self._deterministic_final_abstract(
|
| 6615 |
+
mode, reader_topic_en or source_topic or topic, data_metadata, hypothesis_ledger=hypothesis_ledger
|
| 6616 |
),
|
| 6617 |
"keywords": self._canonical_quantitative_front_matter_keywords(
|
| 6618 |
+
mode, list(reader_keywords_en), data_metadata
|
| 6619 |
) or self._front_matter_keyword_fallback_terms(data_metadata),
|
| 6620 |
}
|
| 6621 |
self._record_adaptive_completion_residual(
|
|
|
|
| 6634 |
)
|
| 6635 |
if not final_abstract or not final_keywords:
|
| 6636 |
final_abstract = final_abstract or self._deterministic_final_abstract(
|
| 6637 |
+
mode, reader_topic_en or source_topic or topic, data_metadata, hypothesis_ledger=hypothesis_ledger
|
| 6638 |
)
|
| 6639 |
final_keywords = final_keywords or self._canonical_quantitative_front_matter_keywords(
|
| 6640 |
+
mode, list(reader_keywords_en), data_metadata
|
| 6641 |
) or self._front_matter_keyword_fallback_terms(data_metadata)
|
| 6642 |
self._record_adaptive_completion_residual(
|
| 6643 |
quality_notes, [{
|
|
|
|
| 6665 |
)
|
| 6666 |
try:
|
| 6667 |
final_front = agent.generate_final_front_matter(
|
| 6668 |
+
reader_topic_en, reader_keywords_prompt, body_only, data_evidence=user_data_md, mode=mode,
|
| 6669 |
validation_feedback=feedback, hypothesis_contract=hypothesis_contract,
|
| 6670 |
authoritative_keywords=self._user_authoritative_front_matter_keywords(data_metadata),
|
| 6671 |
keyword_policy=self._resolved_front_matter_keyword_policy(data_metadata),
|
|
|
|
| 6677 |
)
|
| 6678 |
final_front = {
|
| 6679 |
"abstract": self._deterministic_final_abstract(
|
| 6680 |
+
mode, reader_topic_en or source_topic or topic, data_metadata, hypothesis_ledger=hypothesis_ledger
|
| 6681 |
),
|
| 6682 |
"keywords": final_keywords or self._canonical_quantitative_front_matter_keywords(
|
| 6683 |
+
mode, list(reader_keywords_en), data_metadata
|
| 6684 |
) or self._front_matter_keyword_fallback_terms(data_metadata),
|
| 6685 |
}
|
| 6686 |
final_abstract = str(final_front.get("abstract") or "").strip()
|
|
|
|
| 6693 |
)
|
| 6694 |
if not final_abstract or not final_keywords:
|
| 6695 |
final_abstract = final_abstract or self._deterministic_final_abstract(
|
| 6696 |
+
mode, reader_topic_en or source_topic or topic, data_metadata, hypothesis_ledger=hypothesis_ledger
|
| 6697 |
)
|
| 6698 |
final_keywords = final_keywords or self._canonical_quantitative_front_matter_keywords(
|
| 6699 |
+
mode, list(reader_keywords_en), data_metadata
|
| 6700 |
) or self._front_matter_keyword_fallback_terms(data_metadata)
|
| 6701 |
self._record_adaptive_completion_residual(
|
| 6702 |
quality_notes, [{
|
|
|
|
| 6747 |
)
|
| 6748 |
if residual_final_rows:
|
| 6749 |
deterministic_abstract = self._deterministic_final_abstract(
|
| 6750 |
+
mode, reader_topic_en or source_topic or topic, data_metadata, hypothesis_ledger=hypothesis_ledger
|
| 6751 |
)
|
| 6752 |
hard_rows = self._study_fact_violations(
|
| 6753 |
mode, agent, quant_engine, deterministic_abstract, data_metadata, section_name="Final Abstract"
|
|
|
|
| 6776 |
"examples":final_front_fact_rows[:3],
|
| 6777 |
})
|
| 6778 |
state.pop("final_abstract_retry_exhausted", None)
|
| 6779 |
+
final_title = str(reader_topic_en or source_topic or topic or "").strip()
|
| 6780 |
if mode == "amj" and str(data_metadata.get("source_type") or "") == "system_generated":
|
| 6781 |
semantic_contract = dict(data_metadata.get("front_matter_contract") or {})
|
| 6782 |
reconciled_front = agent.reconcile_quantitative_reader_front_matter(
|
| 6783 |
topic=source_topic, keywords=source_keywords, contract=semantic_contract,
|
| 6784 |
+
manuscript=body_only, reader_title=reader_topic_en, reader_keywords=reader_keywords_en,
|
| 6785 |
)
|
| 6786 |
candidate_title = re.sub(r"\s+", " ", str(reconciled_front.get("title") or "")).strip()
|
| 6787 |
candidate_keywords = self._canonical_quantitative_front_matter_keywords(
|
|
|
|
| 6800 |
full_draft_en = replace_abstract(full_draft_en, final_abstract)
|
| 6801 |
full_draft_en = self._replace_title(full_draft_en, final_title)
|
| 6802 |
full_draft_en = self._replace_keywords(full_draft_en, final_keywords)
|
| 6803 |
+
agent.validate_reader_facing_english_projection(
|
| 6804 |
+
{"title": final_title, "keywords": final_keywords},
|
| 6805 |
+
expected_keyword_count=(len(reader_keywords_en) if reader_keywords_en else None),
|
| 6806 |
+
)
|
| 6807 |
+
final_abstract, language_rebuilt = self._delivery_first_final_abstract_language_guard(
|
| 6808 |
+
mode=mode, agent=agent, quant_engine=quant_engine, final_abstract=final_abstract,
|
| 6809 |
+
reader_topic_en=reader_topic_en, final_title=final_title, data_metadata=data_metadata,
|
| 6810 |
+
hypothesis_ledger=hypothesis_ledger, quality_notes=quality_notes,
|
| 6811 |
+
)
|
| 6812 |
+
if language_rebuilt:
|
| 6813 |
+
full_draft_en = replace_abstract(full_draft_en, final_abstract)
|
| 6814 |
if mode == "amj" and hypothesis_ledger:
|
| 6815 |
post_front_hypothesis_rows = self._hypothesis_contract_violations(
|
| 6816 |
full_draft_en, hypothesis_ledger, section_name="Full Manuscript", require_all=True
|
core/pipeline_agent.py
CHANGED
|
@@ -1640,6 +1640,117 @@ Return a concise PROVISIONAL RESEARCH LOGIC CONTRACT in plain English prose. It
|
|
| 1640 |
return cleaned
|
| 1641 |
|
| 1642 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1643 |
@staticmethod
|
| 1644 |
def _front_matter_has_citation(value):
|
| 1645 |
text = str(value or '')
|
|
@@ -1679,6 +1790,8 @@ Return a concise PROVISIONAL RESEARCH LOGIC CONTRACT in plain English prose. It
|
|
| 1679 |
word_count = cls._working_abstract_word_count(text)
|
| 1680 |
if cls._front_matter_has_citation(text):
|
| 1681 |
raise DeepSeekRequestError('Working Abstract must not contain citations.')
|
|
|
|
|
|
|
| 1682 |
forbidden = (
|
| 1683 |
('RESULT_ASSERTION', r'\bwe\s+(?:find|found|show|demonstrate|observe|establish)\b'),
|
| 1684 |
('RESULT_ASSERTION', r'\b(?:results?|findings?)\s+(?:show|indicate|reveal|demonstrate|confirm|support)\b'),
|
|
@@ -1875,24 +1988,28 @@ Return a concise PROVISIONAL RESEARCH LOGIC CONTRACT in plain English prose. It
|
|
| 1875 |
return issues
|
| 1876 |
|
| 1877 |
|
| 1878 |
-
def reconcile_quantitative_reader_front_matter(
|
| 1879 |
-
|
|
|
|
|
|
|
| 1880 |
|
| 1881 |
-
|
| 1882 |
-
|
| 1883 |
-
|
| 1884 |
-
|
| 1885 |
-
checkpoints and compatibility behavior remain stable.
|
| 1886 |
"""
|
| 1887 |
record = dict(contract or {})
|
| 1888 |
intent = dict(record.get("user_research_intent") or {})
|
| 1889 |
policy = keyword_policy_from_contract(record, fallback_keywords=keywords)
|
| 1890 |
source_topic = str(intent.get("source_topic") or record.get("source_topic") or topic or "").strip()
|
| 1891 |
source_keywords = list(intent.get("source_keywords") or [])
|
|
|
|
|
|
|
| 1892 |
if str(policy.get("authority_source") or "").upper() == "USER_EXPLICIT" and source_keywords:
|
|
|
|
| 1893 |
return {
|
| 1894 |
-
"title":
|
| 1895 |
-
"keywords": apply_keyword_policy(
|
| 1896 |
}
|
| 1897 |
|
| 1898 |
# No explicit user keyword authority: retain user-authoritative title when
|
|
@@ -1910,7 +2027,7 @@ Return a concise PROVISIONAL RESEARCH LOGIC CONTRACT in plain English prose. It
|
|
| 1910 |
logging.warning("Front-matter semantic contract missing frozen constructs; using degraded fallback.")
|
| 1911 |
fallback = self._quantitative_front_matter_fallback(record)
|
| 1912 |
if intent and source_topic:
|
| 1913 |
-
fallback["title"] =
|
| 1914 |
return fallback
|
| 1915 |
|
| 1916 |
source_keywords = list(record.get("source_keywords") or [])
|
|
@@ -1999,7 +2116,7 @@ A contextual/domain term is allowed. Mark INVALID only when the proposal makes a
|
|
| 1999 |
extra = [str(item).strip() for item in ((critic or {}).get("unmeasured_focal_constructs") or []) if str(item).strip()]
|
| 2000 |
missing = [str(item).strip() for item in ((critic or {}).get("missing_frozen_constructs") or []) if str(item).strip()]
|
| 2001 |
if not invalid and not extra and not missing:
|
| 2002 |
-
resolved_title =
|
| 2003 |
return {
|
| 2004 |
"title": resolved_title,
|
| 2005 |
"keywords": apply_keyword_policy(candidate.get("keywords") or [], policy),
|
|
@@ -2013,7 +2130,11 @@ A contextual/domain term is allowed. Mark INVALID only when the proposal makes a
|
|
| 2013 |
|
| 2014 |
fallback = self._quantitative_front_matter_fallback(record)
|
| 2015 |
if intent and source_topic:
|
| 2016 |
-
fallback["title"] =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2017 |
logging.warning("Adaptive front-matter reconciliation exhausted; using deterministic degraded fallback instead of failing manuscript generation.")
|
| 2018 |
return fallback
|
| 2019 |
|
|
@@ -2028,7 +2149,7 @@ A contextual/domain term is allowed. Mark INVALID only when the proposal makes a
|
|
| 2028 |
prompt = f"""Create WORKING front matter for an empirical {str(mode).upper()}-oriented manuscript before body drafting.
|
| 2029 |
Return JSON only: {{\"abstract\":\"...\",\"keywords\":[\"...\"]}}.
|
| 2030 |
Aim for approximately 120-180 English words, have no citations, and function only as a continuity anchor. Word count is guidance, not a failure condition; prioritize Conciseness, Accuracy, and Rigor over mechanical compliance. It may state the research problem, theoretical tension, focal concepts, question/hypothesis architecture, design or method class, and intended contribution domain. It MUST NOT state coefficients, p-values, fit statistics, sample statistics unless already indispensable to define the design, supported/unsupported hypotheses, result direction, substantive findings, or a conclusion that has not yet been frozen. Do not predict findings.
|
| 2031 |
-
Preserve the
|
| 2032 |
TOPIC: {topic}
|
| 2033 |
USER KEYWORDS: {keywords}
|
| 2034 |
<PROVISIONAL_RESEARCH_LOGIC>
|
|
@@ -2065,7 +2186,7 @@ If the immutable hypothesis contract is non-empty, preserve its H-identifiers an
|
|
| 2065 |
prompt = f"""Write FINAL English front matter for this completed {str(mode).upper()} manuscript.
|
| 2066 |
Return JSON only: {{\"abstract\":\"...\",\"keywords\":[\"...\"]}}.
|
| 2067 |
Aim for a concise Abstract of roughly 120-200 words, use no citations, and accurately state the research problem, validated design, main frozen finding(s), and contribution. Word count is guidance, not a failure condition; prioritize Conciseness, Accuracy, and Rigor. Include only the minimum numerical evidence required; do not list coefficients, p-values, and fit statistics unless one value is indispensable. Do not infer beyond the frozen evidence or completed manuscript.
|
| 2068 |
-
Preserve the
|
| 2069 |
TOPIC: {topic}
|
| 2070 |
USER KEYWORDS: {keywords}
|
| 2071 |
<FROZEN_RESEARCH_EVIDENCE>
|
|
@@ -2085,6 +2206,8 @@ If the immutable hypothesis contract is non-empty, preserve its H-identifiers an
|
|
| 2085 |
abstract = re.sub(r'(?im)^#+\s*abstract\s*', '', str(parsed.get('abstract') or '')).strip()
|
| 2086 |
if self._front_matter_has_citation(abstract):
|
| 2087 |
raise DeepSeekRequestError('Final Abstract must not contain citations.')
|
|
|
|
|
|
|
| 2088 |
meta_findings = self.inspect_authorial_ai_meta(abstract)
|
| 2089 |
if meta_findings:
|
| 2090 |
raise DeepSeekRequestError('Final Abstract contains high-confidence authorial AI-writing meta-commentary.')
|
|
|
|
| 1640 |
return cleaned
|
| 1641 |
|
| 1642 |
|
| 1643 |
+
@staticmethod
|
| 1644 |
+
def _english_reader_surface_has_foreign_script(value):
|
| 1645 |
+
"""Return True only for scripts that cannot remain in an English reader-facing surface.
|
| 1646 |
+
|
| 1647 |
+
User intent may be entered in Chinese, Russian, or Kazakh, but the English
|
| 1648 |
+
manuscript projection must not leak Han or Cyrillic source text. Latin
|
| 1649 |
+
acronyms, numerals, punctuation, and ordinary English remain untouched.
|
| 1650 |
+
"""
|
| 1651 |
+
return bool(re.search(r'[\u3400-\u9fff\u3040-\u30ff\u0400-\u052f]', str(value or '')))
|
| 1652 |
+
|
| 1653 |
+
@classmethod
|
| 1654 |
+
def validate_reader_facing_english_projection(cls, projection, *, expected_keyword_count=None):
|
| 1655 |
+
"""Validate a language projection without changing the user's semantic authority."""
|
| 1656 |
+
record = dict(projection or {})
|
| 1657 |
+
title = str(record.get('title') or record.get('english_title') or '').strip()
|
| 1658 |
+
raw_keywords = record.get('keywords')
|
| 1659 |
+
if raw_keywords is None:
|
| 1660 |
+
raw_keywords = record.get('english_keywords')
|
| 1661 |
+
keywords = [str(item).strip() for item in (raw_keywords or []) if str(item).strip()]
|
| 1662 |
+
if not title:
|
| 1663 |
+
raise DeepSeekRequestError('English reader-facing title is empty.')
|
| 1664 |
+
if expected_keyword_count is not None and len(keywords) != int(expected_keyword_count):
|
| 1665 |
+
raise DeepSeekRequestError(
|
| 1666 |
+
f'English reader-facing keyword cardinality changed: expected={int(expected_keyword_count)} observed={len(keywords)}.'
|
| 1667 |
+
)
|
| 1668 |
+
combined = ' '.join([title] + keywords)
|
| 1669 |
+
if cls._english_reader_surface_has_foreign_script(combined):
|
| 1670 |
+
raise DeepSeekRequestError('English reader-facing title/keywords contain untranslated Han or Cyrillic source text.')
|
| 1671 |
+
return {'title': title, 'keywords': keywords}
|
| 1672 |
+
|
| 1673 |
+
def normalize_reader_facing_front_matter_to_english(self, topic, keywords):
|
| 1674 |
+
"""Create a faithful English language projection of user Title/Keywords.
|
| 1675 |
+
|
| 1676 |
+
The original user strings remain semantic authority in the study contract.
|
| 1677 |
+
This method is presentation-only: English input is passed through without a
|
| 1678 |
+
model call; non-English/mixed-script input is translated, never optimized.
|
| 1679 |
+
Keyword cardinality and order are invariant.
|
| 1680 |
+
"""
|
| 1681 |
+
source_title = str(topic or '').strip()
|
| 1682 |
+
source_keywords = parse_keyword_terms(keywords, explicit_user=True)
|
| 1683 |
+
if not source_title:
|
| 1684 |
+
raise DeepSeekRequestError('User research title is empty before English language projection.')
|
| 1685 |
+
|
| 1686 |
+
source_surface = ' '.join([source_title] + source_keywords)
|
| 1687 |
+
if not self._english_reader_surface_has_foreign_script(source_surface):
|
| 1688 |
+
return {
|
| 1689 |
+
'title': source_title,
|
| 1690 |
+
'keywords': list(source_keywords),
|
| 1691 |
+
'translation_applied': False,
|
| 1692 |
+
}
|
| 1693 |
+
|
| 1694 |
+
indexed_keywords = [
|
| 1695 |
+
{'index': index, 'text': value}
|
| 1696 |
+
for index, value in enumerate(source_keywords, start=1)
|
| 1697 |
+
]
|
| 1698 |
+
payload = {'title': source_title, 'keywords': indexed_keywords}
|
| 1699 |
+
base_prompt = f"""Translate ONLY the supplied user research Title and Keywords into academic English.
|
| 1700 |
+
Return JSON only: {{"title":"...","keywords":["..."]}}.
|
| 1701 |
+
|
| 1702 |
+
LANGUAGE-AUTHORITY CONTRACT:
|
| 1703 |
+
1. This is translation, not title optimization, rewriting, topic expansion, or keyword generation.
|
| 1704 |
+
2. Preserve the complete research meaning: focal constructs, direction/relationship wording, mediator/moderator roles, population/context, method qualifiers, acronyms, numbers, and scope.
|
| 1705 |
+
3. Do not add, delete, merge, split, reorder, generalize, specialize, or reinterpret any keyword. Return exactly {len(source_keywords)} keywords in the supplied order.
|
| 1706 |
+
4. Preserve existing English technical identities and acronyms. Translate/transliterate Han or Cyrillic reader-facing text so the returned Title/Keywords contain no Chinese, Russian, or Kazakh script.
|
| 1707 |
+
5. Do not mention the translation process and do not wrap JSON in Markdown.
|
| 1708 |
+
|
| 1709 |
+
<SOURCE_USER_FRONT_MATTER>
|
| 1710 |
+
{json.dumps(payload, ensure_ascii=False)}
|
| 1711 |
+
</SOURCE_USER_FRONT_MATTER>"""
|
| 1712 |
+
current_prompt = base_prompt
|
| 1713 |
+
last_error = None
|
| 1714 |
+
for local_attempt in range(2):
|
| 1715 |
+
response = self.call_deepseek(
|
| 1716 |
+
'You are a precise academic translator. Preserve research semantics exactly and change only language surface.',
|
| 1717 |
+
current_prompt,
|
| 1718 |
+
max_tokens=max(700, min(1600, 500 + len(source_keywords) * 90)),
|
| 1719 |
+
temp=0.0,
|
| 1720 |
+
model_role='writing',
|
| 1721 |
+
thinking=False,
|
| 1722 |
+
allow_fallback=False,
|
| 1723 |
+
response_format={'type': 'json_object'},
|
| 1724 |
+
logical_node='front_matter_writing',
|
| 1725 |
+
)
|
| 1726 |
+
try:
|
| 1727 |
+
parsed = self._extract_json_from_text(response)
|
| 1728 |
+
if not isinstance(parsed, dict):
|
| 1729 |
+
raise DeepSeekRequestError('English front-matter language projection was not valid JSON.')
|
| 1730 |
+
candidate = self.validate_reader_facing_english_projection(
|
| 1731 |
+
parsed, expected_keyword_count=len(source_keywords)
|
| 1732 |
+
)
|
| 1733 |
+
return {
|
| 1734 |
+
'title': candidate['title'],
|
| 1735 |
+
'keywords': candidate['keywords'],
|
| 1736 |
+
'translation_applied': True,
|
| 1737 |
+
}
|
| 1738 |
+
except DeepSeekRequestError as exc:
|
| 1739 |
+
last_error = exc
|
| 1740 |
+
if local_attempt >= 1:
|
| 1741 |
+
raise
|
| 1742 |
+
logging.warning(
|
| 1743 |
+
'English front-matter language projection retry | attempt=%s/2 | reason=%s',
|
| 1744 |
+
local_attempt + 1, self._safe_source_error(exc),
|
| 1745 |
+
)
|
| 1746 |
+
current_prompt = base_prompt + (
|
| 1747 |
+
'\n\nLOCAL VALIDATION RETRY: The prior response violated the language/cardinality contract. '
|
| 1748 |
+
'Return a fresh complete JSON object only. Translate the same Title and the same indexed Keywords '
|
| 1749 |
+
'faithfully into English, preserve keyword order/count exactly, and remove all Han/Cyrillic source text. '
|
| 1750 |
+
'Do not optimize or rewrite the research intent.'
|
| 1751 |
+
)
|
| 1752 |
+
raise DeepSeekRequestError(f'English front-matter language projection failed: {last_error}')
|
| 1753 |
+
|
| 1754 |
@staticmethod
|
| 1755 |
def _front_matter_has_citation(value):
|
| 1756 |
text = str(value or '')
|
|
|
|
| 1790 |
word_count = cls._working_abstract_word_count(text)
|
| 1791 |
if cls._front_matter_has_citation(text):
|
| 1792 |
raise DeepSeekRequestError('Working Abstract must not contain citations.')
|
| 1793 |
+
if cls._english_reader_surface_has_foreign_script(text):
|
| 1794 |
+
raise DeepSeekRequestError('Working Abstract contains untranslated Han or Cyrillic source text.')
|
| 1795 |
forbidden = (
|
| 1796 |
('RESULT_ASSERTION', r'\bwe\s+(?:find|found|show|demonstrate|observe|establish)\b'),
|
| 1797 |
('RESULT_ASSERTION', r'\b(?:results?|findings?)\s+(?:show|indicate|reveal|demonstrate|confirm|support)\b'),
|
|
|
|
| 1988 |
return issues
|
| 1989 |
|
| 1990 |
|
| 1991 |
+
def reconcile_quantitative_reader_front_matter(
|
| 1992 |
+
self, *, topic, keywords, contract, manuscript="", reader_title=None, reader_keywords=None
|
| 1993 |
+
):
|
| 1994 |
+
"""Preserve source semantic authority while allowing a faithful English surface.
|
| 1995 |
|
| 1996 |
+
``user_research_intent`` remains the immutable semantic authority. When
|
| 1997 |
+
``reader_title``/``reader_keywords`` are supplied they are a language-only
|
| 1998 |
+
English projection of that same authority; they may change script/language
|
| 1999 |
+
but not constructs, relations, scope, order, or keyword cardinality.
|
|
|
|
| 2000 |
"""
|
| 2001 |
record = dict(contract or {})
|
| 2002 |
intent = dict(record.get("user_research_intent") or {})
|
| 2003 |
policy = keyword_policy_from_contract(record, fallback_keywords=keywords)
|
| 2004 |
source_topic = str(intent.get("source_topic") or record.get("source_topic") or topic or "").strip()
|
| 2005 |
source_keywords = list(intent.get("source_keywords") or [])
|
| 2006 |
+
reader_source_topic = str(reader_title or source_topic or topic or "").strip()
|
| 2007 |
+
reader_source_keywords = parse_keyword_terms(reader_keywords, explicit_user=True) if reader_keywords is not None else list(source_keywords)
|
| 2008 |
if str(policy.get("authority_source") or "").upper() == "USER_EXPLICIT" and source_keywords:
|
| 2009 |
+
authoritative_reader = reader_source_keywords or source_keywords
|
| 2010 |
return {
|
| 2011 |
+
"title": reader_source_topic,
|
| 2012 |
+
"keywords": apply_keyword_policy(authoritative_reader, policy, authoritative=authoritative_reader),
|
| 2013 |
}
|
| 2014 |
|
| 2015 |
# No explicit user keyword authority: retain user-authoritative title when
|
|
|
|
| 2027 |
logging.warning("Front-matter semantic contract missing frozen constructs; using degraded fallback.")
|
| 2028 |
fallback = self._quantitative_front_matter_fallback(record)
|
| 2029 |
if intent and source_topic:
|
| 2030 |
+
fallback["title"] = reader_source_topic
|
| 2031 |
return fallback
|
| 2032 |
|
| 2033 |
source_keywords = list(record.get("source_keywords") or [])
|
|
|
|
| 2116 |
extra = [str(item).strip() for item in ((critic or {}).get("unmeasured_focal_constructs") or []) if str(item).strip()]
|
| 2117 |
missing = [str(item).strip() for item in ((critic or {}).get("missing_frozen_constructs") or []) if str(item).strip()]
|
| 2118 |
if not invalid and not extra and not missing:
|
| 2119 |
+
resolved_title = reader_source_topic if intent else re.sub(r"\s+", " ", str(candidate.get("title") or "")).strip()
|
| 2120 |
return {
|
| 2121 |
"title": resolved_title,
|
| 2122 |
"keywords": apply_keyword_policy(candidate.get("keywords") or [], policy),
|
|
|
|
| 2130 |
|
| 2131 |
fallback = self._quantitative_front_matter_fallback(record)
|
| 2132 |
if intent and source_topic:
|
| 2133 |
+
fallback["title"] = reader_source_topic
|
| 2134 |
+
if str(policy.get("authority_source") or "").upper() == "USER_EXPLICIT" and reader_source_keywords:
|
| 2135 |
+
fallback["keywords"] = apply_keyword_policy(
|
| 2136 |
+
reader_source_keywords, policy, authoritative=reader_source_keywords
|
| 2137 |
+
)
|
| 2138 |
logging.warning("Adaptive front-matter reconciliation exhausted; using deterministic degraded fallback instead of failing manuscript generation.")
|
| 2139 |
return fallback
|
| 2140 |
|
|
|
|
| 2149 |
prompt = f"""Create WORKING front matter for an empirical {str(mode).upper()}-oriented manuscript before body drafting.
|
| 2150 |
Return JSON only: {{\"abstract\":\"...\",\"keywords\":[\"...\"]}}.
|
| 2151 |
Aim for approximately 120-180 English words, have no citations, and function only as a continuity anchor. Word count is guidance, not a failure condition; prioritize Conciseness, Accuracy, and Rigor over mechanical compliance. It may state the research problem, theoretical tension, focal concepts, question/hypothesis architecture, design or method class, and intended contribution domain. It MUST NOT state coefficients, p-values, fit statistics, sample statistics unless already indispensable to define the design, supported/unsupported hypotheses, result direction, substantive findings, or a conclusion that has not yet been frozen. Do not predict findings.
|
| 2152 |
+
Preserve the supplied English keyword identities and research scope. They are a faithful language projection of the user input. Do not silently replace, delete, reorder, or reinterpret an explicit construct/relation because direct literature support is sparse. Do not translate them back into the source input language.
|
| 2153 |
TOPIC: {topic}
|
| 2154 |
USER KEYWORDS: {keywords}
|
| 2155 |
<PROVISIONAL_RESEARCH_LOGIC>
|
|
|
|
| 2186 |
prompt = f"""Write FINAL English front matter for this completed {str(mode).upper()} manuscript.
|
| 2187 |
Return JSON only: {{\"abstract\":\"...\",\"keywords\":[\"...\"]}}.
|
| 2188 |
Aim for a concise Abstract of roughly 120-200 words, use no citations, and accurately state the research problem, validated design, main frozen finding(s), and contribution. Word count is guidance, not a failure condition; prioritize Conciseness, Accuracy, and Rigor. Include only the minimum numerical evidence required; do not list coefficients, p-values, and fit statistics unless one value is indispensable. Do not infer beyond the frozen evidence or completed manuscript.
|
| 2189 |
+
Preserve the supplied English keyword identities and research scope. They are a faithful language projection of the user input. Do not silently replace, delete, reorder, or reinterpret an explicit construct/relation because direct literature support is sparse. Do not translate them back into the source input language.
|
| 2190 |
TOPIC: {topic}
|
| 2191 |
USER KEYWORDS: {keywords}
|
| 2192 |
<FROZEN_RESEARCH_EVIDENCE>
|
|
|
|
| 2206 |
abstract = re.sub(r'(?im)^#+\s*abstract\s*', '', str(parsed.get('abstract') or '')).strip()
|
| 2207 |
if self._front_matter_has_citation(abstract):
|
| 2208 |
raise DeepSeekRequestError('Final Abstract must not contain citations.')
|
| 2209 |
+
if self._english_reader_surface_has_foreign_script(abstract):
|
| 2210 |
+
raise DeepSeekRequestError('Final Abstract contains untranslated Han or Cyrillic source text.')
|
| 2211 |
meta_findings = self.inspect_authorial_ai_meta(abstract)
|
| 2212 |
if meta_findings:
|
| 2213 |
raise DeepSeekRequestError('Final Abstract contains high-confidence authorial AI-writing meta-commentary.')
|
core/production_guard.py
CHANGED
|
@@ -848,6 +848,40 @@ def _aigc_visible_label_for_docx(filename: str) -> str:
|
|
| 848 |
return visible_label_for_language(filename)
|
| 849 |
|
| 850 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 851 |
def _validate_zip_bytes(
|
| 852 |
zip_bytes: bytes,
|
| 853 |
*,
|
|
@@ -859,6 +893,7 @@ def _validate_zip_bytes(
|
|
| 859 |
quantitative_manifest: dict[str, Any] | None,
|
| 860 |
qualitative_mode: bool,
|
| 861 |
safe_mode: bool,
|
|
|
|
| 862 |
) -> None:
|
| 863 |
try:
|
| 864 |
with zipfile.ZipFile(io.BytesIO(zip_bytes), "r") as archive:
|
|
@@ -874,12 +909,13 @@ def _validate_zip_bytes(
|
|
| 874 |
for name in (english_filename, translated_filename):
|
| 875 |
archived_docx = archive.read(name)
|
| 876 |
validate_docx_bytes(archived_docx)
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
|
|
|
| 883 |
if quantitative_manifest:
|
| 884 |
archived_markdown = english_markdown if name == english_filename else translated_markdown
|
| 885 |
validate_quantitative_table_contract(
|
|
@@ -920,22 +956,21 @@ def _validated_package_attempt(
|
|
| 920 |
translated_docx = _render_delivery_docx(
|
| 921 |
translated_markdown, qualitative_mode=qualitative_mode, safe_mode=safe_mode,
|
| 922 |
)
|
| 923 |
-
# E3-B: label only the already-rendered
|
| 924 |
-
#
|
| 925 |
-
|
| 926 |
-
|
| 927 |
-
|
| 928 |
-
|
| 929 |
-
|
| 930 |
-
|
| 931 |
-
|
| 932 |
validate_docx_bytes(english_docx)
|
| 933 |
validate_docx_bytes(translated_docx)
|
| 934 |
-
|
| 935 |
-
|
| 936 |
-
|
| 937 |
-
|
| 938 |
-
raise OutputValidationError(f"Post-generation AI output label validation failed: {exc}") from exc
|
| 939 |
|
| 940 |
quantitative_manifest = (
|
| 941 |
(data_metadata or {}).get("quantitative_table_manifest")
|
|
@@ -955,6 +990,9 @@ def _validated_package_attempt(
|
|
| 955 |
|
| 956 |
appendix_prefix = "Appendix/" if organize_appendix else ""
|
| 957 |
notes = list(quality_notes or [])
|
|
|
|
|
|
|
|
|
|
| 958 |
if safe_mode:
|
| 959 |
notes.append({
|
| 960 |
"severity": "ADVISORY",
|
|
@@ -995,6 +1033,7 @@ def _validated_package_attempt(
|
|
| 995 |
quantitative_manifest=quantitative_manifest,
|
| 996 |
qualitative_mode=qualitative_mode,
|
| 997 |
safe_mode=safe_mode,
|
|
|
|
| 998 |
)
|
| 999 |
return PackageResult(
|
| 1000 |
zip_bytes=zip_bytes,
|
|
@@ -1118,37 +1157,36 @@ def build_recovery_package(
|
|
| 1118 |
raise OutputValidationError("Recovery package requires a materially complete frozen English manuscript.")
|
| 1119 |
try:
|
| 1120 |
english_docx = _render_delivery_docx(english, qualitative_mode=False, safe_mode=True)
|
| 1121 |
-
english_label = _aigc_visible_label_for_docx(english_filename)
|
| 1122 |
-
english_docx = ensure_aigc_output_label(english_docx, visible_label=english_label)
|
| 1123 |
validate_docx_bytes(english_docx)
|
| 1124 |
-
validate_aigc_output_label_resilient(english_docx, expected_visible_label=english_label)
|
| 1125 |
except Exception:
|
| 1126 |
english_docx = _plain_recovery_docx(english)
|
| 1127 |
-
english_label = _aigc_visible_label_for_docx(english_filename)
|
| 1128 |
-
english_docx = ensure_aigc_output_label(english_docx, visible_label=english_label)
|
| 1129 |
validate_docx_bytes(english_docx)
|
| 1130 |
-
|
|
|
|
|
|
|
| 1131 |
|
| 1132 |
entries: dict[str, bytes] = {english_filename: english_docx}
|
|
|
|
|
|
|
| 1133 |
target = str(translated_markdown or "").strip()
|
| 1134 |
if len(target) >= 800:
|
| 1135 |
try:
|
| 1136 |
target_docx = _render_delivery_docx(target, qualitative_mode=False, safe_mode=True)
|
| 1137 |
-
target_label = _aigc_visible_label_for_docx(translated_filename)
|
| 1138 |
-
target_docx = ensure_aigc_output_label(target_docx, visible_label=target_label)
|
| 1139 |
validate_docx_bytes(target_docx)
|
| 1140 |
-
validate_aigc_output_label_resilient(target_docx, expected_visible_label=target_label)
|
| 1141 |
except Exception:
|
| 1142 |
try:
|
| 1143 |
target_docx = _plain_recovery_docx(target)
|
| 1144 |
-
target_label = _aigc_visible_label_for_docx(translated_filename)
|
| 1145 |
-
target_docx = ensure_aigc_output_label(target_docx, visible_label=target_label)
|
| 1146 |
validate_docx_bytes(target_docx)
|
| 1147 |
-
validate_aigc_output_label_resilient(target_docx, expected_visible_label=target_label)
|
| 1148 |
except Exception:
|
| 1149 |
target_docx = b""
|
| 1150 |
if target_docx:
|
|
|
|
|
|
|
|
|
|
| 1151 |
entries[translated_filename] = target_docx
|
|
|
|
|
|
|
|
|
|
| 1152 |
|
| 1153 |
note = (
|
| 1154 |
"SmartPaper Recovery Package\n\n"
|
|
@@ -1157,6 +1195,12 @@ def build_recovery_package(
|
|
| 1157 |
"This recovery artifact must not be represented as a completed paid paper.\n\n"
|
| 1158 |
f"Terminal detail: {str(error_detail or '')[:1000]}\n"
|
| 1159 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1160 |
entries["RECOVERY_NOTES.txt"] = note.encode("utf-8")
|
| 1161 |
manifest = _manifest(entries)
|
| 1162 |
entries["FILE_MANIFEST_SHA256.txt"] = manifest.encode("utf-8")
|
|
@@ -1171,15 +1215,17 @@ def build_recovery_package(
|
|
| 1171 |
raise OutputValidationError(f"Recovery ZIP integrity failed at {bad}.")
|
| 1172 |
archived_english = archive.read(english_filename)
|
| 1173 |
validate_docx_bytes(archived_english)
|
| 1174 |
-
|
| 1175 |
-
|
| 1176 |
-
|
|
|
|
| 1177 |
if translated_filename in archive.namelist():
|
| 1178 |
archived_target = archive.read(translated_filename)
|
| 1179 |
validate_docx_bytes(archived_target)
|
| 1180 |
-
|
| 1181 |
-
|
| 1182 |
-
|
|
|
|
| 1183 |
return PackageResult(
|
| 1184 |
zip_bytes=zip_bytes,
|
| 1185 |
file_names=tuple(sorted(entries)),
|
|
|
|
| 848 |
return visible_label_for_language(filename)
|
| 849 |
|
| 850 |
|
| 851 |
+
def _apply_aigc_label_delivery_first(docx_bytes: bytes, filename: str):
|
| 852 |
+
"""Apply/validate the AI-output label without making it a delivery blocker.
|
| 853 |
+
|
| 854 |
+
The label subsystem already has two independent writer/validator paths. If
|
| 855 |
+
both mechanisms fail unexpectedly, P0 Delivery-First keeps the already
|
| 856 |
+
validated DOCX bytes and records an advisory. Academic manuscript text and
|
| 857 |
+
P1 result/reference identity are never changed by this fallback.
|
| 858 |
+
"""
|
| 859 |
+
original = bytes(docx_bytes or b"")
|
| 860 |
+
validate_docx_bytes(original)
|
| 861 |
+
label = _aigc_visible_label_for_docx(filename)
|
| 862 |
+
try:
|
| 863 |
+
labeled = ensure_aigc_output_label(original, visible_label=label)
|
| 864 |
+
validate_docx_bytes(labeled)
|
| 865 |
+
validate_aigc_output_label_resilient(labeled, expected_visible_label=label)
|
| 866 |
+
return labeled, label, True, None
|
| 867 |
+
except Exception as exc:
|
| 868 |
+
# Never keep a partially-mutated artifact after label failure. Reuse the
|
| 869 |
+
# pre-label DOCX only after independently proving it still opens.
|
| 870 |
+
validate_docx_bytes(original)
|
| 871 |
+
warning = {
|
| 872 |
+
"severity": "ADVISORY",
|
| 873 |
+
"section": "Output package",
|
| 874 |
+
"issue_type": "aigc_output_label_best_effort_fallback",
|
| 875 |
+
"detail": (
|
| 876 |
+
"AI output labeling could not be completed after the independent label "
|
| 877 |
+
f"fallbacks ({type(exc).__name__}). The validated pre-label DOCX was preserved "
|
| 878 |
+
"so generation/download was not interrupted."
|
| 879 |
+
),
|
| 880 |
+
"user_action": "Add/verify the required AI-output label before external distribution if this warning appears.",
|
| 881 |
+
}
|
| 882 |
+
return original, label, False, warning
|
| 883 |
+
|
| 884 |
+
|
| 885 |
def _validate_zip_bytes(
|
| 886 |
zip_bytes: bytes,
|
| 887 |
*,
|
|
|
|
| 893 |
quantitative_manifest: dict[str, Any] | None,
|
| 894 |
qualitative_mode: bool,
|
| 895 |
safe_mode: bool,
|
| 896 |
+
aigc_label_status: dict[str, bool] | None = None,
|
| 897 |
) -> None:
|
| 898 |
try:
|
| 899 |
with zipfile.ZipFile(io.BytesIO(zip_bytes), "r") as archive:
|
|
|
|
| 909 |
for name in (english_filename, translated_filename):
|
| 910 |
archived_docx = archive.read(name)
|
| 911 |
validate_docx_bytes(archived_docx)
|
| 912 |
+
if (aigc_label_status or {}).get(name, True):
|
| 913 |
+
try:
|
| 914 |
+
validate_aigc_output_label_resilient(
|
| 915 |
+
archived_docx, expected_visible_label=_aigc_visible_label_for_docx(name)
|
| 916 |
+
)
|
| 917 |
+
except Exception as exc:
|
| 918 |
+
raise OutputValidationError(f"Archived DOCX AI output label invalid: {exc}") from exc
|
| 919 |
if quantitative_manifest:
|
| 920 |
archived_markdown = english_markdown if name == english_filename else translated_markdown
|
| 921 |
validate_quantitative_table_contract(
|
|
|
|
| 956 |
translated_docx = _render_delivery_docx(
|
| 957 |
translated_markdown, qualitative_mode=qualitative_mode, safe_mode=safe_mode,
|
| 958 |
)
|
| 959 |
+
# E3-B/E3-C + Delivery-First hardening: label only the already-rendered
|
| 960 |
+
# delivery artifact. A total label-subsystem failure is advisory rather than
|
| 961 |
+
# a reason to destroy an otherwise valid downloadable paper.
|
| 962 |
+
english_docx, english_label, english_labeled, english_label_warning = _apply_aigc_label_delivery_first(
|
| 963 |
+
english_docx, english_filename
|
| 964 |
+
)
|
| 965 |
+
translated_docx, translated_label, translated_labeled, translated_label_warning = _apply_aigc_label_delivery_first(
|
| 966 |
+
translated_docx, translated_filename
|
| 967 |
+
)
|
| 968 |
validate_docx_bytes(english_docx)
|
| 969 |
validate_docx_bytes(translated_docx)
|
| 970 |
+
aigc_label_status = {
|
| 971 |
+
english_filename: bool(english_labeled),
|
| 972 |
+
translated_filename: bool(translated_labeled),
|
| 973 |
+
}
|
|
|
|
| 974 |
|
| 975 |
quantitative_manifest = (
|
| 976 |
(data_metadata or {}).get("quantitative_table_manifest")
|
|
|
|
| 990 |
|
| 991 |
appendix_prefix = "Appendix/" if organize_appendix else ""
|
| 992 |
notes = list(quality_notes or [])
|
| 993 |
+
for label_warning in (english_label_warning, translated_label_warning):
|
| 994 |
+
if label_warning:
|
| 995 |
+
notes.append(label_warning)
|
| 996 |
if safe_mode:
|
| 997 |
notes.append({
|
| 998 |
"severity": "ADVISORY",
|
|
|
|
| 1033 |
quantitative_manifest=quantitative_manifest,
|
| 1034 |
qualitative_mode=qualitative_mode,
|
| 1035 |
safe_mode=safe_mode,
|
| 1036 |
+
aigc_label_status=aigc_label_status,
|
| 1037 |
)
|
| 1038 |
return PackageResult(
|
| 1039 |
zip_bytes=zip_bytes,
|
|
|
|
| 1157 |
raise OutputValidationError("Recovery package requires a materially complete frozen English manuscript.")
|
| 1158 |
try:
|
| 1159 |
english_docx = _render_delivery_docx(english, qualitative_mode=False, safe_mode=True)
|
|
|
|
|
|
|
| 1160 |
validate_docx_bytes(english_docx)
|
|
|
|
| 1161 |
except Exception:
|
| 1162 |
english_docx = _plain_recovery_docx(english)
|
|
|
|
|
|
|
| 1163 |
validate_docx_bytes(english_docx)
|
| 1164 |
+
english_docx, english_label, english_labeled, english_label_warning = _apply_aigc_label_delivery_first(
|
| 1165 |
+
english_docx, english_filename
|
| 1166 |
+
)
|
| 1167 |
|
| 1168 |
entries: dict[str, bytes] = {english_filename: english_docx}
|
| 1169 |
+
recovery_label_status = {english_filename: bool(english_labeled)}
|
| 1170 |
+
recovery_label_warnings = [english_label_warning] if english_label_warning else []
|
| 1171 |
target = str(translated_markdown or "").strip()
|
| 1172 |
if len(target) >= 800:
|
| 1173 |
try:
|
| 1174 |
target_docx = _render_delivery_docx(target, qualitative_mode=False, safe_mode=True)
|
|
|
|
|
|
|
| 1175 |
validate_docx_bytes(target_docx)
|
|
|
|
| 1176 |
except Exception:
|
| 1177 |
try:
|
| 1178 |
target_docx = _plain_recovery_docx(target)
|
|
|
|
|
|
|
| 1179 |
validate_docx_bytes(target_docx)
|
|
|
|
| 1180 |
except Exception:
|
| 1181 |
target_docx = b""
|
| 1182 |
if target_docx:
|
| 1183 |
+
target_docx, target_label, target_labeled, target_label_warning = _apply_aigc_label_delivery_first(
|
| 1184 |
+
target_docx, translated_filename
|
| 1185 |
+
)
|
| 1186 |
entries[translated_filename] = target_docx
|
| 1187 |
+
recovery_label_status[translated_filename] = bool(target_labeled)
|
| 1188 |
+
if target_label_warning:
|
| 1189 |
+
recovery_label_warnings.append(target_label_warning)
|
| 1190 |
|
| 1191 |
note = (
|
| 1192 |
"SmartPaper Recovery Package\n\n"
|
|
|
|
| 1195 |
"This recovery artifact must not be represented as a completed paid paper.\n\n"
|
| 1196 |
f"Terminal detail: {str(error_detail or '')[:1000]}\n"
|
| 1197 |
)
|
| 1198 |
+
if recovery_label_warnings:
|
| 1199 |
+
note += (
|
| 1200 |
+
"\nAI-output labeling warning: the label subsystem failed after independent fallbacks. "
|
| 1201 |
+
"The validated pre-label DOCX was preserved so the recovery download remains usable. "
|
| 1202 |
+
"Add/verify the required AI-output label before external distribution.\n"
|
| 1203 |
+
)
|
| 1204 |
entries["RECOVERY_NOTES.txt"] = note.encode("utf-8")
|
| 1205 |
manifest = _manifest(entries)
|
| 1206 |
entries["FILE_MANIFEST_SHA256.txt"] = manifest.encode("utf-8")
|
|
|
|
| 1215 |
raise OutputValidationError(f"Recovery ZIP integrity failed at {bad}.")
|
| 1216 |
archived_english = archive.read(english_filename)
|
| 1217 |
validate_docx_bytes(archived_english)
|
| 1218 |
+
if recovery_label_status.get(english_filename, True):
|
| 1219 |
+
validate_aigc_output_label_resilient(
|
| 1220 |
+
archived_english, expected_visible_label=_aigc_visible_label_for_docx(english_filename)
|
| 1221 |
+
)
|
| 1222 |
if translated_filename in archive.namelist():
|
| 1223 |
archived_target = archive.read(translated_filename)
|
| 1224 |
validate_docx_bytes(archived_target)
|
| 1225 |
+
if recovery_label_status.get(translated_filename, True):
|
| 1226 |
+
validate_aigc_output_label_resilient(
|
| 1227 |
+
archived_target, expected_visible_label=_aigc_visible_label_for_docx(translated_filename)
|
| 1228 |
+
)
|
| 1229 |
return PackageResult(
|
| 1230 |
zip_bytes=zip_bytes,
|
| 1231 |
file_names=tuple(sorted(entries)),
|