Spaces:
Running
Running
S0-R1-C5.3 scoped deployment
#81
by GuXiaopeng - opened
- core/generation_runner.py +58 -16
- core/pipeline_agent.py +85 -14
- core/quantitative_engine.py +5 -0
core/generation_runner.py
CHANGED
|
@@ -1014,6 +1014,40 @@ class GenerationRunner:
|
|
| 1014 |
return rows
|
| 1015 |
return rows
|
| 1016 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1017 |
@staticmethod
|
| 1018 |
def _hypothesis_contract_directive(ledger: dict) -> str:
|
| 1019 |
record = dict(ledger or {})
|
|
@@ -3287,12 +3321,12 @@ class GenerationRunner:
|
|
| 3287 |
if is_quarantined or section_id in {"results", "findings"}:
|
| 3288 |
citation_rule = (
|
| 3289 |
"NO EXTERNAL LITERATURE CITATIONS ALLOWED IN THIS UNIT. "
|
| 3290 |
-
"Report only
|
| 3291 |
)
|
| 3292 |
else:
|
| 3293 |
citation_rule = (
|
| 3294 |
"Use the citation-first claim-evidence plan below as bounded evidence options, not as a claim quota. "
|
| 3295 |
-
"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
|
| 3296 |
"When a source is used, keep the claim no broader than the supplied evidence and attach exactly its assigned [CITE-XX] tag. "
|
| 3297 |
"Never stack sources, substitute tags, or use literature as evidence for this study's own facts.\n"
|
| 3298 |
f"<CLAIM_EVIDENCE_PLAN>\n{claim_plan_text}\n</CLAIM_EVIDENCE_PLAN>"
|
|
@@ -3300,7 +3334,7 @@ class GenerationRunner:
|
|
| 3300 |
if not claim_plan_text.strip():
|
| 3301 |
citation_rule += (
|
| 3302 |
"\nEVIDENCE-LIMITED WRITING RULE: no directly validated source is assigned to this unit. "
|
| 3303 |
-
"Continue the scholarly argument using
|
| 3304 |
"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. "
|
| 3305 |
"Use appropriately qualified language for propositions, interpretations, and research opportunities."
|
| 3306 |
)
|
|
@@ -3330,7 +3364,7 @@ class GenerationRunner:
|
|
| 3330 |
if mode == "amj" and section_id == "results" and data_metadata.get("quantitative_table_manifest"):
|
| 3331 |
table_rule = (
|
| 3332 |
"AUTHORITATIVE TABLE CONTRACT: Do not create, reproduce, renumber, or alter any Markdown statistical table. "
|
| 3333 |
-
"
|
| 3334 |
"You may discuss and refer to Table 1-Table 4 where relevant, but do not invent an additional table or change a table number."
|
| 3335 |
)
|
| 3336 |
elif sec["is_method_or_result"] and mode == "amj":
|
|
@@ -3351,9 +3385,9 @@ class GenerationRunner:
|
|
| 3351 |
)
|
| 3352 |
qualitative_rule = (
|
| 3353 |
f"Assigned qualitative concepts: {concepts}. Assigned evidence profiles: {' | '.join(evidence_profiles)}. "
|
| 3354 |
-
f"
|
| 3355 |
"Use the assigned evidence analytically, but output narrative prose only: do not reproduce any verbatim quotation and do not output any QUOTE-XX identifier. "
|
| 3356 |
-
"
|
| 3357 |
if concepts or evidence_profiles else ""
|
| 3358 |
)
|
| 3359 |
|
|
@@ -3384,9 +3418,11 @@ class GenerationRunner:
|
|
| 3384 |
data_metadata,
|
| 3385 |
include_themes=False,
|
| 3386 |
)
|
|
|
|
|
|
|
| 3387 |
evidence_packet = (
|
| 3388 |
-
f"{
|
| 3389 |
-
if
|
| 3390 |
)
|
| 3391 |
completed_label = ", ".join(completed_sections)
|
| 3392 |
anti_repeat = (
|
|
@@ -3396,11 +3432,16 @@ class GenerationRunner:
|
|
| 3396 |
if re.search(r"\(Part\s+2/2\)", display_name, flags=re.I):
|
| 3397 |
anti_repeat += " Part 2/2 must extend and complement Part 1/2; do not recap its background, mechanism explanation, evidence, or contribution framing."
|
| 3398 |
voice = str(APP_CONFIG.get("journal_voice_profiles", {}).get(mode) or "")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3399 |
sys_prompt = (
|
| 3400 |
APP_CONFIG.get("sys_academic_writer", "") + "\n"
|
| 3401 |
+ evidence_packet + "\n" + voice + "\n"
|
| 3402 |
+ APP_CONFIG.get("deep_refinement_directive", "")
|
| 3403 |
)
|
|
|
|
| 3404 |
word_budget = APP_CONFIG.get("word_budget", {})
|
| 3405 |
min_ratio = float(word_budget.get("section_min_ratio", 0.92))
|
| 3406 |
max_ratio = float(word_budget.get("section_max_ratio", 1.08))
|
|
@@ -3416,24 +3457,24 @@ Planning length: approximately {int(adjusted_target * min_ratio)}-{int(adjusted_
|
|
| 3416 |
{heading_directive}
|
| 3417 |
{citation_rule}
|
| 3418 |
{table_rule}
|
| 3419 |
-
AUTHOR-FACING PURITY: Write only reader-facing scholarly prose.
|
| 3420 |
{self._hypothesis_contract_directive(hypothesis_ledger)}
|
| 3421 |
{str(sec.get('required_content') or '')}
|
| 3422 |
{qualitative_rule}
|
| 3423 |
{anti_repeat}
|
| 3424 |
{self._canonical_concept_writer_directive(canonical_concept_ledger)}
|
| 3425 |
<PROVISIONAL_RESEARCH_LOGIC>
|
| 3426 |
-
{
|
| 3427 |
</PROVISIONAL_RESEARCH_LOGIC>
|
| 3428 |
<WORKING_ABSTRACT_ANCHOR>
|
| 3429 |
-
{
|
| 3430 |
</WORKING_ABSTRACT_ANCHOR>
|
| 3431 |
-
ANCHOR PRECEDENCE:
|
| 3432 |
{memory_block}
|
| 3433 |
Literature synthesis: {synthesis_result}
|
| 3434 |
-
<
|
| 3435 |
-
{
|
| 3436 |
-
</
|
| 3437 |
Previous tail: {prompt_context_tail}
|
| 3438 |
Output only final academic prose. Do not report the word count."""
|
| 3439 |
if agent._count_tokens(sys_prompt) + agent._count_tokens(user_prompt) > 60000:
|
|
@@ -3500,7 +3541,8 @@ Output only final academic prose. Do not report the word count."""
|
|
| 3500 |
candidate_user_prompt += f"""
|
| 3501 |
|
| 3502 |
<SECTION_RETRY_VALIDATION_FEEDBACK>
|
| 3503 |
-
|
|
|
|
| 3504 |
{attempt_feedback}
|
| 3505 |
</SECTION_RETRY_VALIDATION_FEEDBACK>"""
|
| 3506 |
if agent._count_tokens(sys_prompt) + agent._count_tokens(candidate_user_prompt) > 60000:
|
|
|
|
| 1014 |
return rows
|
| 1015 |
return rows
|
| 1016 |
|
| 1017 |
+
@staticmethod
|
| 1018 |
+
def _writer_visible_evidence_context(text: str) -> str:
|
| 1019 |
+
"""Remove private orchestration labels before evidence reaches a Writer.
|
| 1020 |
+
|
| 1021 |
+
Internal evidence/state vocabulary remains unchanged in server-side metadata
|
| 1022 |
+
and validators. This view changes only the control labels presented to the
|
| 1023 |
+
prose model, so AMJ and ASQ receive ordinary scholarly study descriptions
|
| 1024 |
+
instead of phrases that the author-facing purity Gate later has to reject.
|
| 1025 |
+
"""
|
| 1026 |
+
value = str(text or "")
|
| 1027 |
+
substitutions = (
|
| 1028 |
+
(r"(?im)^(#{1,6}\s+)Frozen\s+", r"\1"),
|
| 1029 |
+
(r"(?im)^\|\s*Field\s*\|\s*Frozen value\s*\|$", "| Field | Study value |"),
|
| 1030 |
+
(r"\bfrozen\s+research\s+evidence\s+packet\b", "study evidence"),
|
| 1031 |
+
(r"\bfrozen\s+evidence\s+packet\b", "study evidence"),
|
| 1032 |
+
(r"\bevidence\s+packet\b", "study evidence"),
|
| 1033 |
+
(r"\b(?:study\s+)?evidence\s+ledger\b", "study evidence record"),
|
| 1034 |
+
(r"\bfrozen\s+(?:research\s+)?evidence\b", "study evidence"),
|
| 1035 |
+
(r"\bfrozen\s+study\s+evidence\b", "study evidence"),
|
| 1036 |
+
(r"\bfrozen\s+(?:research\s+|study\s+)?(facts?|numbers?|design|setting|sample|measures?|results?|themes?|quotations?|procedures?)\b", r"documented \1"),
|
| 1037 |
+
(r"\b(?:study|result)-owned\s+facts?\b", "study-specific facts"),
|
| 1038 |
+
(r"\bfrozen\s+task-level\s+concepts?\b", "canonical research concepts"),
|
| 1039 |
+
(r"\bfrozen\s+theme\s+summary\b", "theme summary"),
|
| 1040 |
+
(r"\bfrozen\s+thematic\s+structure\b", "thematic structure"),
|
| 1041 |
+
(r"\bfrozen\s+statistical\s+evidence\b", "study statistical evidence"),
|
| 1042 |
+
)
|
| 1043 |
+
for pattern, replacement in substitutions:
|
| 1044 |
+
value = re.sub(pattern, replacement, value, flags=re.I)
|
| 1045 |
+
# Evidence renderers sometimes append an Executor-only sentence after a
|
| 1046 |
+
# reader-relevant instruction. Drop only that private suffix.
|
| 1047 |
+
value = re.sub(r"(?im);\s*the Executor\b[^\n]*", ".", value)
|
| 1048 |
+
value = re.sub(r"(?im)^\s*the Executor\b[^\n]*$", "", value)
|
| 1049 |
+
return value
|
| 1050 |
+
|
| 1051 |
@staticmethod
|
| 1052 |
def _hypothesis_contract_directive(ledger: dict) -> str:
|
| 1053 |
record = dict(ledger or {})
|
|
|
|
| 3321 |
if is_quarantined or section_id in {"results", "findings"}:
|
| 3322 |
citation_rule = (
|
| 3323 |
"NO EXTERNAL LITERATURE CITATIONS ALLOWED IN THIS UNIT. "
|
| 3324 |
+
"Report only documented empirical/qualitative evidence and the interpretation permitted by the section role."
|
| 3325 |
)
|
| 3326 |
else:
|
| 3327 |
citation_rule = (
|
| 3328 |
"Use the citation-first claim-evidence plan below as bounded evidence options, not as a claim quota. "
|
| 3329 |
+
"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 research logic, section role, or already established manuscript argument; omit any item that would introduce another application domain or mutate the study's facts. "
|
| 3330 |
"When a source is used, keep the claim no broader than the supplied evidence and attach exactly its assigned [CITE-XX] tag. "
|
| 3331 |
"Never stack sources, substitute tags, or use literature as evidence for this study's own facts.\n"
|
| 3332 |
f"<CLAIM_EVIDENCE_PLAN>\n{claim_plan_text}\n</CLAIM_EVIDENCE_PLAN>"
|
|
|
|
| 3334 |
if not claim_plan_text.strip():
|
| 3335 |
citation_rule += (
|
| 3336 |
"\nEVIDENCE-LIMITED WRITING RULE: no directly validated source is assigned to this unit. "
|
| 3337 |
+
"Continue the scholarly argument using the documented study evidence, cautious conceptual reasoning, and general scholarly knowledge. "
|
| 3338 |
"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. "
|
| 3339 |
"Use appropriately qualified language for propositions, interpretations, and research opportunities."
|
| 3340 |
)
|
|
|
|
| 3364 |
if mode == "amj" and section_id == "results" and data_metadata.get("quantitative_table_manifest"):
|
| 3365 |
table_rule = (
|
| 3366 |
"AUTHORITATIVE TABLE CONTRACT: Do not create, reproduce, renumber, or alter any Markdown statistical table. "
|
| 3367 |
+
"Required statistical tables are handled separately from narrative prose. "
|
| 3368 |
"You may discuss and refer to Table 1-Table 4 where relevant, but do not invent an additional table or change a table number."
|
| 3369 |
)
|
| 3370 |
elif sec["is_method_or_result"] and mode == "amj":
|
|
|
|
| 3385 |
)
|
| 3386 |
qualitative_rule = (
|
| 3387 |
f"Assigned qualitative concepts: {concepts}. Assigned evidence profiles: {' | '.join(evidence_profiles)}. "
|
| 3388 |
+
f"This unit has {planned_quote_count} assigned quotation slot(s). "
|
| 3389 |
"Use the assigned evidence analytically, but output narrative prose only: do not reproduce any verbatim quotation and do not output any QUOTE-XX identifier. "
|
| 3390 |
+
"Preserve stage/case/deviant meaning and use deviant evidence to qualify rather than erase the pattern."
|
| 3391 |
if concepts or evidence_profiles else ""
|
| 3392 |
)
|
| 3393 |
|
|
|
|
| 3418 |
data_metadata,
|
| 3419 |
include_themes=False,
|
| 3420 |
)
|
| 3421 |
+
writer_base_evidence_contract = self._writer_visible_evidence_context(base_evidence_contract)
|
| 3422 |
+
writer_scoped_data_md = self._writer_visible_evidence_context(scoped_data_md)
|
| 3423 |
evidence_packet = (
|
| 3424 |
+
f"{writer_base_evidence_contract}\n<STUDY_EVIDENCE>\n{writer_scoped_data_md}\n</STUDY_EVIDENCE>"
|
| 3425 |
+
if writer_scoped_data_md else writer_base_evidence_contract
|
| 3426 |
)
|
| 3427 |
completed_label = ", ".join(completed_sections)
|
| 3428 |
anti_repeat = (
|
|
|
|
| 3432 |
if re.search(r"\(Part\s+2/2\)", display_name, flags=re.I):
|
| 3433 |
anti_repeat += " Part 2/2 must extend and complement Part 1/2; do not recap its background, mechanism explanation, evidence, or contribution framing."
|
| 3434 |
voice = str(APP_CONFIG.get("journal_voice_profiles", {}).get(mode) or "")
|
| 3435 |
+
section_role = self._writer_visible_evidence_context(section_role)
|
| 3436 |
+
mode_directive = self._writer_visible_evidence_context(mode_directive)
|
| 3437 |
+
research_logic_for_writer = self._writer_visible_evidence_context(research_logic_contract)
|
| 3438 |
+
working_abstract_for_writer = self._writer_visible_evidence_context(working_abstract)
|
| 3439 |
sys_prompt = (
|
| 3440 |
APP_CONFIG.get("sys_academic_writer", "") + "\n"
|
| 3441 |
+ evidence_packet + "\n" + voice + "\n"
|
| 3442 |
+ APP_CONFIG.get("deep_refinement_directive", "")
|
| 3443 |
)
|
| 3444 |
+
sys_prompt = self._writer_visible_evidence_context(sys_prompt)
|
| 3445 |
word_budget = APP_CONFIG.get("word_budget", {})
|
| 3446 |
min_ratio = float(word_budget.get("section_min_ratio", 0.92))
|
| 3447 |
max_ratio = float(word_budget.get("section_max_ratio", 1.08))
|
|
|
|
| 3457 |
{heading_directive}
|
| 3458 |
{citation_rule}
|
| 3459 |
{table_rule}
|
| 3460 |
+
AUTHOR-FACING PURITY: Write only reader-facing scholarly prose. Do not mention internal orchestration, validation, state-management, evidence-management, or execution terminology. Describe the study, method, evidence, and limitations directly.
|
| 3461 |
{self._hypothesis_contract_directive(hypothesis_ledger)}
|
| 3462 |
{str(sec.get('required_content') or '')}
|
| 3463 |
{qualitative_rule}
|
| 3464 |
{anti_repeat}
|
| 3465 |
{self._canonical_concept_writer_directive(canonical_concept_ledger)}
|
| 3466 |
<PROVISIONAL_RESEARCH_LOGIC>
|
| 3467 |
+
{research_logic_for_writer}
|
| 3468 |
</PROVISIONAL_RESEARCH_LOGIC>
|
| 3469 |
<WORKING_ABSTRACT_ANCHOR>
|
| 3470 |
+
{working_abstract_for_writer}
|
| 3471 |
</WORKING_ABSTRACT_ANCHOR>
|
| 3472 |
+
ANCHOR PRECEDENCE: The STUDY_EVIDENCE and PROVISIONAL_RESEARCH_LOGIC override the Working Abstract if any tension appears. The Working Abstract is continuity guidance only and must never force a result.
|
| 3473 |
{memory_block}
|
| 3474 |
Literature synthesis: {synthesis_result}
|
| 3475 |
+
<STUDY_EVIDENCE>
|
| 3476 |
+
{writer_scoped_data_md}
|
| 3477 |
+
</STUDY_EVIDENCE>
|
| 3478 |
Previous tail: {prompt_context_tail}
|
| 3479 |
Output only final academic prose. Do not report the word count."""
|
| 3480 |
if agent._count_tokens(sys_prompt) + agent._count_tokens(user_prompt) > 60000:
|
|
|
|
| 3541 |
candidate_user_prompt += f"""
|
| 3542 |
|
| 3543 |
<SECTION_RETRY_VALIDATION_FEEDBACK>
|
| 3544 |
+
# Internal retry invariant: the same frozen evidence universe remains authoritative; this control phrase is not exposed to manuscript prose.
|
| 3545 |
+
The previous attempt for this same manuscript unit was rejected by validation. Rewrite ONLY this same unit once. Keep the identical study evidence, research logic, and section role. Do not fetch, invent, or broaden evidence. Citation tags rejected for this claim context are ineligible on the retry and must not be reused or deterministically reinserted: {excluded_text}. Use another already-authorized eligible source only when it directly supports the rewritten claim, or narrow/qualify the claim so no unsupported citation is needed. Resolve every material issue below without changing the task or manuscript scope:
|
| 3546 |
{attempt_feedback}
|
| 3547 |
</SECTION_RETRY_VALIDATION_FEEDBACK>"""
|
| 3548 |
if agent._count_tokens(sys_prompt) + agent._count_tokens(candidate_user_prompt) > 60000:
|
core/pipeline_agent.py
CHANGED
|
@@ -2778,8 +2778,17 @@ INPUT:
|
|
| 2778 |
if len(self._citation_terms(claim) & self._citation_terms(evidence)) < 2:
|
| 2779 |
continue
|
| 2780 |
section_meta = next((item for item in sections if item['name'] == section), {})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2781 |
if not self.citation_source_role_eligible(
|
| 2782 |
-
tag, section_id=str(section_meta.get('section_id') or ''), section_name=section
|
| 2783 |
):
|
| 2784 |
logging.info('Citation planner rejected METHOD_AUTHORITY outside methods: section=%s tag=%s', section, tag)
|
| 2785 |
continue
|
|
@@ -2852,6 +2861,7 @@ INPUT:
|
|
| 2852 |
'processes','provide','provides','related','relationship','relationships','relevant','report',
|
| 2853 |
'reports','show','shows','specific','support','supports','introduction','discussion',
|
| 2854 |
'background','theoretical','framing','methodology','setting','results','hypothesis','hypotheses',
|
|
|
|
| 2855 |
})
|
| 2856 |
|
| 2857 |
@classmethod
|
|
@@ -2870,6 +2880,41 @@ INPUT:
|
|
| 2870 |
name = re.sub(r'[^a-z]+', ' ', str(section_name or '').lower()).strip()
|
| 2871 |
return bool(re.search(r'\b(?:methods?|methodology)\b', name))
|
| 2872 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2873 |
def citation_source_role_eligible(self, tag='', metadata=None, *, section_id='', section_name=''):
|
| 2874 |
"""Return whether a retrieved source role is eligible for one manuscript section.
|
| 2875 |
|
|
@@ -3047,7 +3092,7 @@ INPUT:
|
|
| 3047 |
tail_terms = (
|
| 3048 |
effective_topic_terms[topic_target:] + section_terms + context_terms
|
| 3049 |
if use_retrieval_anchor
|
| 3050 |
-
else section_terms + context_terms + effective_topic_terms[topic_target:]
|
| 3051 |
)
|
| 3052 |
for term in tail_terms:
|
| 3053 |
if term not in selected:
|
|
@@ -3120,6 +3165,25 @@ INPUT:
|
|
| 3120 |
overlap, distinctive = self._citation_recovery_overlap(
|
| 3121 |
section_name, section_context, claim, evidence
|
| 3122 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3123 |
candidates.append((distinctive, overlap, res, claim, evidence, keys))
|
| 3124 |
staged_keys.update(keys)
|
| 3125 |
candidates.sort(reverse=True, key=lambda row: (row[0], row[1]))
|
|
@@ -3418,10 +3482,14 @@ INPUT:
|
|
| 3418 |
allowed = None if allowed_tags is None else {
|
| 3419 |
str(tag or '').strip() for tag in allowed_tags if str(tag or '').strip()
|
| 3420 |
}
|
|
|
|
|
|
|
|
|
|
| 3421 |
valid_required = [
|
| 3422 |
tag for tag in required_tags
|
| 3423 |
if tag in self.citation_map
|
| 3424 |
and (allowed is None or tag in allowed)
|
|
|
|
| 3425 |
and self.citation_source_role_eligible(
|
| 3426 |
tag, section_id=section_id, section_name=section_name
|
| 3427 |
)
|
|
@@ -3433,6 +3501,8 @@ INPUT:
|
|
| 3433 |
continue
|
| 3434 |
if allowed is not None and tag not in allowed:
|
| 3435 |
continue
|
|
|
|
|
|
|
| 3436 |
if not self.citation_source_role_eligible(
|
| 3437 |
tag, metadata=metadata, section_id=section_id, section_name=section_name
|
| 3438 |
):
|
|
@@ -6398,7 +6468,11 @@ TEXT:
|
|
| 6398 |
@classmethod
|
| 6399 |
def _parse_translation_claim_segments(cls, value, expected_claim_ids=None):
|
| 6400 |
text = str(value or '')
|
| 6401 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6402 |
matches = list(pattern.finditer(text))
|
| 6403 |
expected = list(expected_claim_ids or [])
|
| 6404 |
if not matches:
|
|
@@ -6409,12 +6483,15 @@ TEXT:
|
|
| 6409 |
found = [match.group(1) for match in matches]
|
| 6410 |
if expected and found != expected:
|
| 6411 |
raise DeepSeekRequestError(f'Translation claim marker mismatch: expected={expected}, got={found}.')
|
| 6412 |
-
|
|
|
|
| 6413 |
raise DeepSeekRequestError('Translation output contained unexpected text before the first SP-C marker.')
|
| 6414 |
segments = {}
|
| 6415 |
for index, match in enumerate(matches):
|
| 6416 |
end = matches[index + 1].start() if index + 1 < len(matches) else len(text)
|
| 6417 |
segment = text[match.end():end].strip()
|
|
|
|
|
|
|
| 6418 |
if not segment:
|
| 6419 |
raise DeepSeekRequestError(f'Translation claim {match.group(1)} is empty.')
|
| 6420 |
segments[match.group(1)] = segment
|
|
@@ -6430,16 +6507,10 @@ TEXT:
|
|
| 6430 |
@classmethod
|
| 6431 |
def _strip_translation_claim_markers(cls, value):
|
| 6432 |
text = str(value or '')
|
| 6433 |
-
|
| 6434 |
-
|
| 6435 |
-
|
| 6436 |
-
|
| 6437 |
-
for index, match in enumerate(matches):
|
| 6438 |
-
end = matches[index + 1].start() if index + 1 < len(matches) else len(text)
|
| 6439 |
-
part = text[match.end():end].strip()
|
| 6440 |
-
if part:
|
| 6441 |
-
parts.append(part)
|
| 6442 |
-
return ' '.join(parts).strip()
|
| 6443 |
|
| 6444 |
@classmethod
|
| 6445 |
def _translation_glossary_authorized_acronyms(cls, source_claim, glossary=None):
|
|
|
|
| 2778 |
if len(self._citation_terms(claim) & self._citation_terms(evidence)) < 2:
|
| 2779 |
continue
|
| 2780 |
section_meta = next((item for item in sections if item['name'] == section), {})
|
| 2781 |
+
metadata = self.citation_map.get(tag) or {}
|
| 2782 |
+
if self._citation_claim_requires_method_authority(
|
| 2783 |
+
claim, section_id=str(section_meta.get('section_id') or ''), section_name=section
|
| 2784 |
+
) and not self._citation_has_method_authority(metadata):
|
| 2785 |
+
logging.info(
|
| 2786 |
+
'Citation planner rejected non-METHOD_AUTHORITY for methodological principle: section=%s tag=%s',
|
| 2787 |
+
section, tag,
|
| 2788 |
+
)
|
| 2789 |
+
continue
|
| 2790 |
if not self.citation_source_role_eligible(
|
| 2791 |
+
tag, metadata=metadata, section_id=str(section_meta.get('section_id') or ''), section_name=section
|
| 2792 |
):
|
| 2793 |
logging.info('Citation planner rejected METHOD_AUTHORITY outside methods: section=%s tag=%s', section, tag)
|
| 2794 |
continue
|
|
|
|
| 2861 |
'processes','provide','provides','related','relationship','relationships','relevant','report',
|
| 2862 |
'reports','show','shows','specific','support','supports','introduction','discussion',
|
| 2863 |
'background','theoretical','framing','methodology','setting','results','hypothesis','hypotheses',
|
| 2864 |
+
'evaluate','evaluates','evaluated','evaluating','evaluation','whether',
|
| 2865 |
})
|
| 2866 |
|
| 2867 |
@classmethod
|
|
|
|
| 2880 |
name = re.sub(r'[^a-z]+', ' ', str(section_name or '').lower()).strip()
|
| 2881 |
return bool(re.search(r'\b(?:methods?|methodology)\b', name))
|
| 2882 |
|
| 2883 |
+
@classmethod
|
| 2884 |
+
def _citation_claim_requires_method_authority(cls, sentence='', *, section_id='', section_name=''):
|
| 2885 |
+
"""Return True for reader-facing methodological principles/limitations.
|
| 2886 |
+
|
| 2887 |
+
C5.3 keeps study-owned method facts distinct from claims that justify or
|
| 2888 |
+
delimit an analytical method. Only the latter are forced onto the
|
| 2889 |
+
dedicated METHOD_AUTHORITY lane; a study fact such as the exact bootstrap
|
| 2890 |
+
count must stand on the study evidence itself rather than borrow a citation.
|
| 2891 |
+
"""
|
| 2892 |
+
if not cls._citation_method_section(section_id, section_name):
|
| 2893 |
+
return False
|
| 2894 |
+
value = re.sub(r'\s+', ' ', str(sentence or '').casefold()).strip()
|
| 2895 |
+
if not value:
|
| 2896 |
+
return False
|
| 2897 |
+
method_concept = re.search(
|
| 2898 |
+
r'\b(?:cross[- ]sectional|observational|mediat(?:ion|or)|bootstrap(?:ping)?|confidence interval|'
|
| 2899 |
+
r'temporal ordering|experimental manipulation|instrumental variable|causal (?:effect|mediation|identification|pathway)|'
|
| 2900 |
+
r'measurement validity|construct validity|discriminant validity|convergent validity|reliability|common method|endogeneity)\b',
|
| 2901 |
+
value,
|
| 2902 |
+
)
|
| 2903 |
+
if not method_concept:
|
| 2904 |
+
return False
|
| 2905 |
+
rationale = re.search(
|
| 2906 |
+
r'\b(?:consistent with|methodolog(?:y|ical|ically)|cannot|can not|does not|do not|is not|are not|'
|
| 2907 |
+
r'without|insufficient|inability|limitation|limits?|threat|bias|validity|appropriate|requires?|should|must|'
|
| 2908 |
+
r'establish|identify|infer|interpret|justify|warrant|support|transmit|assess(?:es|ed|ing)?)\b',
|
| 2909 |
+
value,
|
| 2910 |
+
)
|
| 2911 |
+
return bool(rationale)
|
| 2912 |
+
|
| 2913 |
+
@staticmethod
|
| 2914 |
+
def _citation_has_method_authority(metadata):
|
| 2915 |
+
lanes = {str(value or '').strip().lower() for value in (dict(metadata or {}).get('evidence_lanes') or [])}
|
| 2916 |
+
return 'method_authority' in lanes
|
| 2917 |
+
|
| 2918 |
def citation_source_role_eligible(self, tag='', metadata=None, *, section_id='', section_name=''):
|
| 2919 |
"""Return whether a retrieved source role is eligible for one manuscript section.
|
| 2920 |
|
|
|
|
| 3092 |
tail_terms = (
|
| 3093 |
effective_topic_terms[topic_target:] + section_terms + context_terms
|
| 3094 |
if use_retrieval_anchor
|
| 3095 |
+
else retrieval_anchor_terms + section_terms + context_terms + effective_topic_terms[topic_target:]
|
| 3096 |
)
|
| 3097 |
for term in tail_terms:
|
| 3098 |
if term not in selected:
|
|
|
|
| 3165 |
overlap, distinctive = self._citation_recovery_overlap(
|
| 3166 |
section_name, section_context, claim, evidence
|
| 3167 |
)
|
| 3168 |
+
candidate_terms = self._citation_terms(
|
| 3169 |
+
f"{res.get('title', '')} {claim} {evidence}"
|
| 3170 |
+
)
|
| 3171 |
+
query_terms = {
|
| 3172 |
+
term for term in self._citation_terms(query)
|
| 3173 |
+
if term not in self._CITATION_RECOVERY_GENERIC_TERMS
|
| 3174 |
+
and term not in self._CITATION_RECOVERY_QUERY_NOISE_TERMS
|
| 3175 |
+
}
|
| 3176 |
+
anchor_terms = {
|
| 3177 |
+
term
|
| 3178 |
+
for anchor in getattr(self, 'retrieval_query_anchors', [])
|
| 3179 |
+
for term in self._citation_terms(anchor)
|
| 3180 |
+
if term not in self._CITATION_RECOVERY_GENERIC_TERMS
|
| 3181 |
+
and term not in self._CITATION_RECOVERY_QUERY_NOISE_TERMS
|
| 3182 |
+
}
|
| 3183 |
+
if len(candidate_terms & query_terms) < 2:
|
| 3184 |
+
continue
|
| 3185 |
+
if len(anchor_terms) >= 2 and len(candidate_terms & anchor_terms) < 2:
|
| 3186 |
+
continue
|
| 3187 |
candidates.append((distinctive, overlap, res, claim, evidence, keys))
|
| 3188 |
staged_keys.update(keys)
|
| 3189 |
candidates.sort(reverse=True, key=lambda row: (row[0], row[1]))
|
|
|
|
| 3482 |
allowed = None if allowed_tags is None else {
|
| 3483 |
str(tag or '').strip() for tag in allowed_tags if str(tag or '').strip()
|
| 3484 |
}
|
| 3485 |
+
requires_method_authority = self._citation_claim_requires_method_authority(
|
| 3486 |
+
sentence, section_id=section_id, section_name=section_name
|
| 3487 |
+
)
|
| 3488 |
valid_required = [
|
| 3489 |
tag for tag in required_tags
|
| 3490 |
if tag in self.citation_map
|
| 3491 |
and (allowed is None or tag in allowed)
|
| 3492 |
+
and (not requires_method_authority or self._citation_has_method_authority(self.citation_map.get(tag)))
|
| 3493 |
and self.citation_source_role_eligible(
|
| 3494 |
tag, section_id=section_id, section_name=section_name
|
| 3495 |
)
|
|
|
|
| 3501 |
continue
|
| 3502 |
if allowed is not None and tag not in allowed:
|
| 3503 |
continue
|
| 3504 |
+
if requires_method_authority and not self._citation_has_method_authority(metadata):
|
| 3505 |
+
continue
|
| 3506 |
if not self.citation_source_role_eligible(
|
| 3507 |
tag, metadata=metadata, section_id=section_id, section_name=section_name
|
| 3508 |
):
|
|
|
|
| 6468 |
@classmethod
|
| 6469 |
def _parse_translation_claim_segments(cls, value, expected_claim_ids=None):
|
| 6470 |
text = str(value or '')
|
| 6471 |
+
# C5.3 accepts the protected claim marker as a delimiter even when a
|
| 6472 |
+
# translation model keeps the following target text on the same line.
|
| 6473 |
+
# Ownership remains fail-closed because marker order/count must still
|
| 6474 |
+
# match exactly and no preamble before the first marker is accepted.
|
| 6475 |
+
pattern = re.compile(r'\[\[SP-(C\d{6})\]\]')
|
| 6476 |
matches = list(pattern.finditer(text))
|
| 6477 |
expected = list(expected_claim_ids or [])
|
| 6478 |
if not matches:
|
|
|
|
| 6483 |
found = [match.group(1) for match in matches]
|
| 6484 |
if expected and found != expected:
|
| 6485 |
raise DeepSeekRequestError(f'Translation claim marker mismatch: expected={expected}, got={found}.')
|
| 6486 |
+
preamble = text[:matches[0].start()].strip()
|
| 6487 |
+
if preamble and len(expected) > 1:
|
| 6488 |
raise DeepSeekRequestError('Translation output contained unexpected text before the first SP-C marker.')
|
| 6489 |
segments = {}
|
| 6490 |
for index, match in enumerate(matches):
|
| 6491 |
end = matches[index + 1].start() if index + 1 < len(matches) else len(text)
|
| 6492 |
segment = text[match.end():end].strip()
|
| 6493 |
+
if index == 0 and preamble and len(expected) <= 1:
|
| 6494 |
+
segment = f"{preamble} {segment}".strip()
|
| 6495 |
if not segment:
|
| 6496 |
raise DeepSeekRequestError(f'Translation claim {match.group(1)} is empty.')
|
| 6497 |
segments[match.group(1)] = segment
|
|
|
|
| 6507 |
@classmethod
|
| 6508 |
def _strip_translation_claim_markers(cls, value):
|
| 6509 |
text = str(value or '')
|
| 6510 |
+
# Claim markers are private delimiters. Remove them whether the model
|
| 6511 |
+
# rendered them on their own line or inline before the translated claim.
|
| 6512 |
+
cleaned = re.sub(r'\[\[SP-C\d{6}\]\]\s*', '', text)
|
| 6513 |
+
return re.sub(r'\s+', ' ', cleaned).strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6514 |
|
| 6515 |
@classmethod
|
| 6516 |
def _translation_glossary_authorized_acronyms(cls, source_claim, glossary=None):
|
core/quantitative_engine.py
CHANGED
|
@@ -627,6 +627,10 @@ Keywords: {keywords}"""
|
|
| 627 |
r"\b(?:cannot|can't|could not|couldn't|does not|doesn't|do not|don't|did not|didn't)\s+(?:identify|establish|demonstrate|show|infer|support|imply|claim|justify|warrant|permit|allow)\s*(?:(?:an?|the)\s*)?$",
|
| 628 |
r"\b(?:is not|isn't|are not|aren't|was not|wasn't|were not|weren't)\s+(?:able\s+to\s+)?(?:identify|establish|demonstrate|show|infer|support|justify|warrant)\s*(?:(?:an?|the)\s*)?$",
|
| 629 |
r"\b(?:unable|insufficient)\s+to\s+(?:identify|establish|demonstrate|show|infer|support|justify|warrant)\s*(?:(?:an?|the)\s*)?$",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 630 |
r"\b(?:fails?|failed)\s+to\s+(?:identify|establish|demonstrate|show|infer|support|justify|warrant)\s*(?:(?:an?|the)\s*)?$",
|
| 631 |
r"\bno\s+(?:evidence|basis|support)\s+(?:for|of)\s*(?:(?:an?|the)\s*)?$",
|
| 632 |
r"\bnot\s+(?:evidence|proof)\s+(?:for|of)\s*(?:(?:an?|the)\s*)?$",
|
|
@@ -638,6 +642,7 @@ Keywords: {keywords}"""
|
|
| 638 |
post_disclaimer_patterns = (
|
| 639 |
r"^(?:\s*(?:is|are|was|were|has|have|had))?\s*(?:not|never)\s+(?:identified|established|demonstrated|shown|inferred|supported|claimed|justified|warranted)\b",
|
| 640 |
r"^\s*(?:cannot|can't|could not|couldn't|should not|shouldn't|must not|does not|doesn't|do not|don't)\s+(?:be\s+)?(?:identified|established|demonstrated|shown|inferred|supported|claimed|interpreted|justified|warranted)\b",
|
|
|
|
| 641 |
)
|
| 642 |
return any(re.search(pattern, right_folded) for pattern in post_disclaimer_patterns)
|
| 643 |
|
|
|
|
| 627 |
r"\b(?:cannot|can't|could not|couldn't|does not|doesn't|do not|don't|did not|didn't)\s+(?:identify|establish|demonstrate|show|infer|support|imply|claim|justify|warrant|permit|allow)\s*(?:(?:an?|the)\s*)?$",
|
| 628 |
r"\b(?:is not|isn't|are not|aren't|was not|wasn't|were not|weren't)\s+(?:able\s+to\s+)?(?:identify|establish|demonstrate|show|infer|support|justify|warrant)\s*(?:(?:an?|the)\s*)?$",
|
| 629 |
r"\b(?:unable|insufficient)\s+to\s+(?:identify|establish|demonstrate|show|infer|support|justify|warrant)\s*(?:(?:an?|the)\s*)?$",
|
| 630 |
+
r"\b(?:inability|lack\s+of\s+(?:an?\s+)?ability)\s+to\s+(?:identify|establish|demonstrate|show|infer|support|justify|warrant)\s*(?:(?:an?|the)\s*)?$",
|
| 631 |
+
r"\bwithout\s+(?:directly\s+)?(?:identifying|establishing|demonstrating|showing|inferring|supporting|justifying|warranting)(?:\s+\w+){0,8}\s*(?:(?:an?|the)\s*)?$",
|
| 632 |
+
r"\b(?:not\s+designed|not\s+intended)\s+to\s+(?:identify|establish|demonstrate|show|infer|support|justify|warrant)\s*(?:(?:an?|the)\s*)?$",
|
| 633 |
+
r"\b(?:should not|shouldn't|must not|cannot|can't|could not|couldn't)\s+be\s+interpreted\s+as\s*(?:(?:an?|the)\s*)?$",
|
| 634 |
r"\b(?:fails?|failed)\s+to\s+(?:identify|establish|demonstrate|show|infer|support|justify|warrant)\s*(?:(?:an?|the)\s*)?$",
|
| 635 |
r"\bno\s+(?:evidence|basis|support)\s+(?:for|of)\s*(?:(?:an?|the)\s*)?$",
|
| 636 |
r"\bnot\s+(?:evidence|proof)\s+(?:for|of)\s*(?:(?:an?|the)\s*)?$",
|
|
|
|
| 642 |
post_disclaimer_patterns = (
|
| 643 |
r"^(?:\s*(?:is|are|was|were|has|have|had))?\s*(?:not|never)\s+(?:identified|established|demonstrated|shown|inferred|supported|claimed|justified|warranted)\b",
|
| 644 |
r"^\s*(?:cannot|can't|could not|couldn't|should not|shouldn't|must not|does not|doesn't|do not|don't)\s+(?:be\s+)?(?:identified|established|demonstrated|shown|inferred|supported|claimed|interpreted|justified|warranted)\b",
|
| 645 |
+
r"^\s*(?:claim|claims|interpretation|inference)s?\s+(?:is|are)\s+(?:not|unsupported|unwarranted)\b",
|
| 646 |
)
|
| 647 |
return any(re.search(pattern, right_folded) for pattern in post_disclaimer_patterns)
|
| 648 |
|