S0-R1-C5.5-R6-R5 Discussion Study Interpretation + Evidence-Limited Citation Closure

#93
Files changed (2) hide show
  1. core/generation_runner.py +40 -1
  2. core/pipeline_agent.py +77 -4
core/generation_runner.py CHANGED
@@ -2355,6 +2355,20 @@ class GenerationRunner:
2355
  else {"introduction", "theory", "methodology", "discussion"}
2356
  )
2357
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2358
  @classmethod
2359
  def _adaptive_citation_requirements(cls, tasks, mode: str):
2360
  """Derive evidence demand and semantic-survival reserve from manuscript units."""
@@ -3029,6 +3043,13 @@ class GenerationRunner:
3029
  }
3030
  for display_name, reserve_target in citation_reserve_targets.items():
3031
  planned_items = citation_plan.setdefault(display_name, [])
 
 
 
 
 
 
 
3032
  shortfall = max(0, reserve_target - len(planned_items))
3033
  if shortfall <= 0:
3034
  continue
@@ -3054,6 +3075,12 @@ class GenerationRunner:
3054
  deficit = max(0, int(required) - int(top_level_counts.get(section_id, 0)))
3055
  if deficit <= 0:
3056
  continue
 
 
 
 
 
 
3057
  section_units = [
3058
  name for name, mapped in citation_unit_section_map.items() if mapped == section_id
3059
  ]
@@ -3643,6 +3670,14 @@ class GenerationRunner:
3643
  "\nCURRENT-STUDY PROVENANCE RULE: Do not attach external literature citations to this study's own research question, H1-Hn statement, sample/design fact, analytic choice, result, limitation specific to the available study evidence, or author positioning. "
3644
  "If prior literature provides a rationale, state that external rationale in a separate sentence and cite only that sentence."
3645
  )
 
 
 
 
 
 
 
 
3646
  if section_id not in {"results", "findings", "appendix"}:
3647
  citation_rule += (
3648
  "\nATOMIC LITERATURE CLAIM CONTRACT: Write one externally verifiable proposition per sentence. "
@@ -4022,7 +4057,11 @@ The previous attempt for this same manuscript unit was rejected by validation. R
4022
 
4023
  used_after = set(agent.used_citations)
4024
  survived = used_after - used_before
4025
- if attempt_assigned_tag_list and not is_quarantined:
 
 
 
 
4026
  required_survival = min(
4027
  len(attempt_assigned_tag_list),
4028
  max(1, int(math.ceil(len(attempt_assigned_tag_list) * CITATION_SEMANTIC_SURVIVAL_RATIO))),
 
2355
  else {"introduction", "theory", "methodology", "discussion"}
2356
  )
2357
 
2358
+ @staticmethod
2359
+ def _quota_driven_citation_recovery_allowed(section_id: str) -> bool:
2360
+ """Discussion evidence is claim-driven, never padded to a word-count target.
2361
+
2362
+ R6-R5 keeps claim-specific planner assignments, but disables reserve/JIT
2363
+ top-ups whose only trigger is a numeric citation target. After drafting,
2364
+ the existing bounded top-level completion may still attach a directly
2365
+ validated source to an actual external claim.
2366
+ """
2367
+ return str(section_id or '').strip().lower() != 'discussion'
2368
+
2369
+ # Backward-readable name used by R6-R5 replay documentation.
2370
+ _pre_draft_citation_recovery_allowed = _quota_driven_citation_recovery_allowed
2371
+
2372
  @classmethod
2373
  def _adaptive_citation_requirements(cls, tasks, mode: str):
2374
  """Derive evidence demand and semantic-survival reserve from manuscript units."""
 
3043
  }
3044
  for display_name, reserve_target in citation_reserve_targets.items():
3045
  planned_items = citation_plan.setdefault(display_name, [])
3046
+ unit_section_id = str(task_by_name.get(display_name, {}).get("section_id") or "")
3047
+ if not self._quota_driven_citation_recovery_allowed(unit_section_id):
3048
+ logging.info(
3049
+ "Discussion pre-draft reserve recovery skipped: unit=%s direct_plan=%s policy=claim_driven",
3050
+ display_name, len(planned_items),
3051
+ )
3052
+ continue
3053
  shortfall = max(0, reserve_target - len(planned_items))
3054
  if shortfall <= 0:
3055
  continue
 
3075
  deficit = max(0, int(required) - int(top_level_counts.get(section_id, 0)))
3076
  if deficit <= 0:
3077
  continue
3078
+ if not self._quota_driven_citation_recovery_allowed(section_id):
3079
+ logging.info(
3080
+ "Discussion pre-draft JIT quota top-up skipped: section=%s direct_plan=%s required=%s policy=claim_driven",
3081
+ section_id, int(top_level_counts.get(section_id, 0)), int(required),
3082
+ )
3083
+ continue
3084
  section_units = [
3085
  name for name, mapped in citation_unit_section_map.items() if mapped == section_id
3086
  ]
 
3670
  "\nCURRENT-STUDY PROVENANCE RULE: Do not attach external literature citations to this study's own research question, H1-Hn statement, sample/design fact, analytic choice, result, limitation specific to the available study evidence, or author positioning. "
3671
  "If prior literature provides a rationale, state that external rationale in a separate sentence and cite only that sentence."
3672
  )
3673
+ if section_id == "discussion":
3674
+ citation_rule += (
3675
+ "\nDISCUSSION EVIDENCE-LIMITED CONTRACT: Distinguish prior-literature claims from study-owned interpretation. "
3676
+ "Statements such as 'the present evidence suggests', 'these findings indicate', or 'the present results suggest' are interpretations of this study and must not borrow an external citation. "
3677
+ "Do not create or broaden an external-literature claim merely to consume a citation or satisfy a citation count. "
3678
+ "When claim-specific literature is sparse, develop the Discussion from documented study evidence, cautious conceptual reasoning, boundary conditions, and limitations. "
3679
+ "If comparing with prior literature, put the directly supported prior-literature proposition in its own cited sentence and the study-owned interpretation in a separate uncited sentence."
3680
+ )
3681
  if section_id not in {"results", "findings", "appendix"}:
3682
  citation_rule += (
3683
  "\nATOMIC LITERATURE CLAIM CONTRACT: Write one externally verifiable proposition per sentence. "
 
4057
 
4058
  used_after = set(agent.used_citations)
4059
  survived = used_after - used_before
4060
+ if (
4061
+ attempt_assigned_tag_list
4062
+ and not is_quarantined
4063
+ and self._quota_driven_citation_recovery_allowed(section_id)
4064
+ ):
4065
  required_survival = min(
4066
  len(attempt_assigned_tag_list),
4067
  max(1, int(math.ceil(len(attempt_assigned_tag_list) * CITATION_SEMANTIC_SURVIVAL_RATIO))),
core/pipeline_agent.py CHANGED
@@ -3327,6 +3327,63 @@ INPUT:
3327
  return 'STRUCTURAL'
3328
  return 'EXTERNAL_LITERATURE_CLAIM'
3329
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3330
  @classmethod
3331
  def _citation_study_evidence_signal(cls, sentence, section_name=''):
3332
  text = re.sub(r'\s+', ' ', str(sentence or '')).casefold().strip()
@@ -3400,9 +3457,24 @@ INPUT:
3400
  clean = re.sub(r'\s*\[?CITE-\d+\]?\s*', ' ', raw, flags=re.I)
3401
  clean = re.sub(r'\s+', ' ', clean).strip()
3402
  base = cls._citation_base_provenance(clean, section_name)
 
 
 
3403
 
 
 
 
 
 
 
 
 
 
 
 
 
3404
  # Structural/hypothesis/positioning/result identities stay authoritative.
3405
- if base in {'STRUCTURAL', 'HYPOTHESIS_STATEMENT', 'STUDY_POSITIONING', 'RESULT_OWNED_FACT'}:
3406
  provenance = base
3407
  components = [base]
3408
  else:
@@ -3439,6 +3511,7 @@ INPUT:
3439
  'METHOD_PRINCIPLE': 'METHOD_AUTHORITY',
3440
  'EXTERNAL_LITERATURE_CLAIM': 'LITERATURE_EVIDENCE',
3441
  'AUTHOR_INFERENCE': 'AUTHOR_REASONING',
 
3442
  'STUDY_POSITIONING': 'AUTHOR_REASONING',
3443
  'HYPOTHESIS_STATEMENT': 'HYPOTHESIS_CONTRACT',
3444
  'STRUCTURAL': 'NONE',
@@ -3493,9 +3566,9 @@ INPUT:
3493
  + ', '.join(frame.get('ownership_components') or []) + '.'
3494
  ),
3495
  'repair_instruction': (
3496
- 'Split the sentence by evidence ownership. State the current-study fact from the study evidence without an external citation; '
3497
- 'state a methodological principle separately and cite only a directly supporting METHOD_AUTHORITY source; keep author inference '
3498
- 'separate, cautious, and uncited unless a source directly supports that exact inference.'
3499
  ),
3500
  })
3501
  return rows
 
3327
  return 'STRUCTURAL'
3328
  return 'EXTERNAL_LITERATURE_CLAIM'
3329
 
3330
+ @staticmethod
3331
+ def _citation_discussion_section(section_id='', section_name=''):
3332
+ sid = str(section_id or '').strip().lower()
3333
+ if sid in {'discussion', 'conclusion'}:
3334
+ return True
3335
+ name = re.sub(r'[^a-z]+', ' ', str(section_name or '').lower()).strip()
3336
+ return bool(re.search(r'\b(?:discussion|conclusion|implications?)\b', name))
3337
+
3338
+ @staticmethod
3339
+ def _citation_external_literature_signal(sentence):
3340
+ """High-confidence signal that a clause explicitly attributes a claim to prior literature."""
3341
+ text = re.sub(r'\s+', ' ', str(sentence or '')).casefold().strip()
3342
+ if not text:
3343
+ return False
3344
+ return bool(re.search(
3345
+ r'\b(?:prior|previous|existing|earlier) (?:studies|research|work|literature)|'
3346
+ r'\bthe (?:prior|existing|broader) literature\b|'
3347
+ r'\b(?:studies|researchers|scholars) (?:have )?(?:shown|show|reported|report|argued|argue|suggested|suggest|found|find|documented|document)\b|'
3348
+ r'\bevidence from (?:prior|previous|existing|earlier) (?:studies|research|work)\b',
3349
+ text, flags=re.I,
3350
+ ))
3351
+
3352
+ @classmethod
3353
+ def _citation_study_interpretation_signal(cls, sentence, section_id='', section_name=''):
3354
+ """Identify Discussion/Conclusion interpretations owned by this study, not by external literature.
3355
+
3356
+ R6-R5 distinguishes an interpretation of the frozen study evidence from a
3357
+ literature finding. The signal is deliberately narrow: it requires both a
3358
+ current-study anchor and an interpretive predicate in a Discussion-like unit.
3359
+ Pure descriptions of prior research remain literature-owned, while mixed
3360
+ comparison sentences are handled by the evidence-ownership splitter.
3361
+ """
3362
+ if not cls._citation_discussion_section(section_id, section_name):
3363
+ return False
3364
+ text = re.sub(r'\s+', ' ', str(sentence or '')).casefold().strip()
3365
+ if not text:
3366
+ return False
3367
+ anchor = bool(re.search(
3368
+ r'\b(?:the present|these|our|the current) (?:evidence|findings|results)|'
3369
+ r'\b(?:the findings|the results|the evidence) (?:of|from|in) (?:this|the present|the current) study|'
3370
+ r'\bthe (?:pattern|association|associations|relationship|relationships) observed here|'
3371
+ r'\bthe observed (?:pattern|association|associations|relationship|relationships)|'
3372
+ r'\btaken together,? (?:the|these|our) (?:findings|results|evidence)|'
3373
+ r'\bthe emergent process model\b|'
3374
+ r'\bthe process model developed here\b',
3375
+ text, flags=re.I,
3376
+ ))
3377
+ if not anchor:
3378
+ return False
3379
+ interpretive = bool(re.search(
3380
+ r'\b(?:suggests?|indicates?|implies?|shows?|reveals?|highlights?|points? to|qualif(?:y|ies|ied)|'
3381
+ r'is consistent with|are consistent with|appears? to|supports? (?:the )?interpretation|'
3382
+ r'warrants?|means? that|by showing)\b',
3383
+ text, flags=re.I,
3384
+ ))
3385
+ return interpretive
3386
+
3387
  @classmethod
3388
  def _citation_study_evidence_signal(cls, sentence, section_name=''):
3389
  text = re.sub(r'\s+', ' ', str(sentence or '')).casefold().strip()
 
3457
  clean = re.sub(r'\s*\[?CITE-\d+\]?\s*', ' ', raw, flags=re.I)
3458
  clean = re.sub(r'\s+', ' ', clean).strip()
3459
  base = cls._citation_base_provenance(clean, section_name)
3460
+ study_interpretation = cls._citation_study_interpretation_signal(
3461
+ clean, section_id=section_id, section_name=section_name
3462
+ )
3463
 
3464
+ # Discussion interpretations of this study's own evidence override a
3465
+ # superficial RESULT/FINDING token in a subsection title. If the same
3466
+ # sentence also attributes a proposition to prior literature, ownership
3467
+ # becomes mixed and the sentence must be split before citation QA.
3468
+ if study_interpretation:
3469
+ components = []
3470
+ if cls._citation_external_literature_signal(clean):
3471
+ components.append('EXTERNAL_LITERATURE_CLAIM')
3472
+ components.append('STUDY_INTERPRETATION')
3473
+ provenance = (
3474
+ 'MIXED_EVIDENCE_OWNERSHIP' if len(components) > 1 else 'STUDY_INTERPRETATION'
3475
+ )
3476
  # Structural/hypothesis/positioning/result identities stay authoritative.
3477
+ elif base in {'STRUCTURAL', 'HYPOTHESIS_STATEMENT', 'STUDY_POSITIONING', 'RESULT_OWNED_FACT'}:
3478
  provenance = base
3479
  components = [base]
3480
  else:
 
3511
  'METHOD_PRINCIPLE': 'METHOD_AUTHORITY',
3512
  'EXTERNAL_LITERATURE_CLAIM': 'LITERATURE_EVIDENCE',
3513
  'AUTHOR_INFERENCE': 'AUTHOR_REASONING',
3514
+ 'STUDY_INTERPRETATION': 'STUDY_EVIDENCE_LEDGER+AUTHOR_REASONING',
3515
  'STUDY_POSITIONING': 'AUTHOR_REASONING',
3516
  'HYPOTHESIS_STATEMENT': 'HYPOTHESIS_CONTRACT',
3517
  'STRUCTURAL': 'NONE',
 
3566
  + ', '.join(frame.get('ownership_components') or []) + '.'
3567
  ),
3568
  'repair_instruction': (
3569
+ 'Split the sentence by evidence ownership. State the current-study fact or study-owned interpretation from the study evidence without an external citation; '
3570
+ 'state a methodological principle separately and cite only a directly supporting METHOD_AUTHORITY source; state prior-literature claims '
3571
+ 'in their own sentence with direct literature support; keep author inference separate and cautious.'
3572
  ),
3573
  })
3574
  return rows