S0-R1-C5.3 scoped deployment

#81
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 frozen empirical/qualitative evidence and the interpretation permitted by the section role."
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 frozen research logic, section role, or already established manuscript argument; omit any item that would introduce another application domain or mutate the study's facts. "
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 frozen study evidence, cautious conceptual reasoning, and general scholarly knowledge. "
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
- "The Executor will attach the required reader-facing tables deterministically from the quantitative evidence manifest. "
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"The Executor has frozen {planned_quote_count} quotation invocation(s) for this task. "
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
- "The Executor inserts the frozen quotations deterministically after narrative/citation QA. Preserve stage/case/deviant meaning and use deviant evidence to qualify rather than erase the pattern."
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"{base_evidence_contract}\n<FROZEN_RESEARCH_EVIDENCE>\n{scoped_data_md}\n</FROZEN_RESEARCH_EVIDENCE>"
3389
- if scoped_data_md else base_evidence_contract
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. Never mention SmartPaper control labels such as frozen evidence, evidence packet, evidence ledger, study-owned/result-owned fact, frozen theme summary, frozen thematic structure, or frozen task-level concepts. Describe the study, method, evidence, and limitations directly.
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
- {research_logic_contract}
3427
  </PROVISIONAL_RESEARCH_LOGIC>
3428
  <WORKING_ABSTRACT_ANCHOR>
3429
- {working_abstract}
3430
  </WORKING_ABSTRACT_ANCHOR>
3431
- ANCHOR PRECEDENCE: Frozen research evidence and the PROVISIONAL_RESEARCH_LOGIC override the Working Abstract if any tension appears. The Working Abstract is continuity guidance only and must never force a result.
3432
  {memory_block}
3433
  Literature synthesis: {synthesis_result}
3434
- <FROZEN_RESEARCH_EVIDENCE>
3435
- {scoped_data_md}
3436
- </FROZEN_RESEARCH_EVIDENCE>
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
- The previous attempt for this same manuscript unit was rejected by validation. Rewrite ONLY this same unit once. Keep the identical frozen evidence universe, 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-frozen 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:
 
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
- pattern = re.compile(r'(?m)^\[\[SP-(C\d{6})\]\]\s*$')
 
 
 
 
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
- if text[:matches[0].start()].strip():
 
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
- matches = list(re.finditer(r'(?m)^\[\[SP-(C\d{6})\]\]\s*$', text))
6434
- if not matches:
6435
- return text.strip()
6436
- parts = []
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