S0-R1-C2 Dual-Engine Four-Language Claim-Citation Semantic Binding

#75
core/generation_runner.py CHANGED
@@ -3122,17 +3122,17 @@ The previous attempt for this same manuscript unit was rejected by validation. R
3122
  f"section_content_invalid_after_retry:{display_name}:{pre_validation_rows[:3]}"
3123
  )
3124
 
3125
- allow_deterministic_citation_reinforcement = not (
3126
- mode == "asq" and section_id in {"methods", "discussion"}
3127
- )
3128
- if not is_quarantined and attempt_assigned_tag_list and allow_deterministic_citation_reinforcement:
3129
- en_raw = agent.reinforce_claim_specific_citation_coverage(
3130
- en_raw, attempt_assigned_tag_list, section_name=display_name, section_id=section_id
3131
- )
3132
  self._progress(pct, f"citation_audit:{current_index}:{total_steps}")
3133
  used_before = set(agent.used_citations)
3134
  en_resolved = agent.audit_and_resolve_citations(
3135
- prefix + en_raw, section_name=display_name, quarantine=is_quarantined
 
3136
  )
3137
  # Capture immediately: citation recovery may later replace the
3138
  # agent's last-violation buffer. These violations belong to the
@@ -3213,7 +3213,7 @@ The previous attempt for this same manuscript unit was rejected by validation. R
3213
  recovered = agent.recover_citations_on_existing_claims(
3214
  en_resolved,
3215
  recovery_items[:needed],
3216
- section_name=display_name,
3217
  needed=needed,
3218
  )
3219
  if recovered != en_resolved:
 
3122
  f"section_content_invalid_after_retry:{display_name}:{pre_validation_rows[:3]}"
3123
  )
3124
 
3125
+ # S0-R1-C2: the authoritative path never attaches a citation merely
3126
+ # to satisfy section-level coverage. The writer-owned claim/evidence
3127
+ # plan is audited as written; any validated coverage shortfall flows
3128
+ # to the existing claim-anchored recovery path below. This rule is
3129
+ # shared by ASQ and AMJ and keeps the sealed English citation binding
3130
+ # authoritative for ZH/RU/KK translation.
 
3131
  self._progress(pct, f"citation_audit:{current_index}:{total_steps}")
3132
  used_before = set(agent.used_citations)
3133
  en_resolved = agent.audit_and_resolve_citations(
3134
+ prefix + en_raw, section_name=display_name, section_id=section_id,
3135
+ quarantine=is_quarantined
3136
  )
3137
  # Capture immediately: citation recovery may later replace the
3138
  # agent's last-violation buffer. These violations belong to the
 
3213
  recovered = agent.recover_citations_on_existing_claims(
3214
  en_resolved,
3215
  recovery_items[:needed],
3216
+ section_name=display_name, section_id=section_id,
3217
  needed=needed,
3218
  )
3219
  if recovered != en_resolved:
core/pipeline_agent.py CHANGED
@@ -3148,7 +3148,7 @@ INPUT:
3148
  return f"{core.rstrip()} {tag}{punctuation}{trailing}"
3149
 
3150
  def recover_citations_on_existing_claims(
3151
- self, text, items, *, section_name='', needed=1,
3152
  ):
3153
  """Attach recovery citations only to claims already present in the unit.
3154
 
@@ -3233,9 +3233,21 @@ INPUT:
3233
  expected_tags = {tag for _start, _end, _sentence, tag in ranked_assignments}
3234
  audit_len = len(self.citation_audit_log)
3235
  try:
3236
- audited = self.semantic_audit_claim_specific_citations(
3237
- proposed, section_name=f"{section_name} — claim-anchored citation recovery"
3238
- )
 
 
 
 
 
 
 
 
 
 
 
 
3239
  except PipelineCancelledError:
3240
  raise
3241
  except Exception as exc:
@@ -3280,16 +3292,32 @@ INPUT:
3280
  )
3281
  return ''
3282
 
3283
- def _candidate_citation_tags(self, sentence, required_tags, limit):
 
 
 
 
 
 
 
 
 
 
3284
  valid_required = [
3285
  tag for tag in required_tags
3286
- if tag in self.citation_map
 
 
3287
  ]
3288
  sentence_terms = self._citation_terms(sentence)
3289
  scored = []
3290
  for tag, metadata in self.citation_map.items():
3291
  if tag in valid_required:
3292
  continue
 
 
 
 
3293
  evidence = " ".join(
3294
  [
3295
  str(metadata.get("ref", "")),
@@ -4145,6 +4173,7 @@ INPUT:
4145
  self,
4146
  text,
4147
  section_name="",
 
4148
  ):
4149
  """Audit citation support while granting the audit TAG-ONLY mutation authority.
4150
 
@@ -4188,6 +4217,7 @@ INPUT:
4188
  sentence,
4189
  required_tags,
4190
  candidate_limit,
 
4191
  )
4192
  evidence = []
4193
  for tag in candidates:
@@ -4228,7 +4258,7 @@ Rules:
4228
  1. Preserve the sentence's substantive meaning, every number, percentage, coefficient, quotation, variable label, and empirical direction.
4229
  2. Every output sentence may contain at most one [CITE-XX] tag.
4230
  3. When several sources support the same claim, retain only the single source whose supplied evidence most directly supports that claim.
4231
- 4. When different sources support genuinely different claims in the original sentence, split it into separate complete sentences and attach one directly supporting source to each sentence.
4232
  5. A source may be used only when its supplied title/abstract evidence directly supports the adjacent claim. Do not infer support from the reference title alone when the evidence is empty or unrelated.
4233
  6. Use only candidate tags supplied for that item. Never invent a source, fact, number, quotation, or citation tag.
4234
  7. If no candidate source directly supports a claim that requires external evidence, set status to unsupported and leave revised_text equal to the original sentence.
@@ -4360,6 +4390,23 @@ Return exactly:
4360
  )
4361
  continue
4362
  if status == "split":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4363
  violation = {
4364
  "type":"citation_claim_split_required", "id":record["id"],
4365
  "sentence":record["original"][:500], "reason":reason,
@@ -4562,7 +4609,7 @@ Return exactly:
4562
  )
4563
  return revised
4564
 
4565
- def audit_and_resolve_citations(self, text, section_name='', quarantine=False):
4566
  self._last_citation_semantic_violations = []
4567
  value = str(text or '')
4568
  if quarantine:
@@ -4576,7 +4623,9 @@ Return exactly:
4576
  violations = self.find_multi_citation_violations(value)
4577
  if has_citations and self.citation_semantic_audit.get('enabled', True):
4578
  try:
4579
- value = self.semantic_audit_claim_specific_citations(value, section_name=section_name)
 
 
4580
  except PipelineCancelledError:
4581
  raise
4582
  except CitationAuditError as error:
 
3148
  return f"{core.rstrip()} {tag}{punctuation}{trailing}"
3149
 
3150
  def recover_citations_on_existing_claims(
3151
+ self, text, items, *, section_name='', section_id='', needed=1,
3152
  ):
3153
  """Attach recovery citations only to claims already present in the unit.
3154
 
 
3233
  expected_tags = {tag for _start, _end, _sentence, tag in ranked_assignments}
3234
  audit_len = len(self.citation_audit_log)
3235
  try:
3236
+ audit_method = self.semantic_audit_claim_specific_citations
3237
+ audit_kwargs = {
3238
+ 'section_name': f"{section_name} — claim-anchored citation recovery",
3239
+ }
3240
+ try:
3241
+ parameters = list(inspect.signature(audit_method).parameters.values())
3242
+ supports_section_id = any(
3243
+ item.name == 'section_id' or item.kind == inspect.Parameter.VAR_KEYWORD
3244
+ for item in parameters
3245
+ )
3246
+ except (TypeError, ValueError):
3247
+ supports_section_id = True
3248
+ if supports_section_id:
3249
+ audit_kwargs['section_id'] = section_id
3250
+ audited = audit_method(proposed, **audit_kwargs)
3251
  except PipelineCancelledError:
3252
  raise
3253
  except Exception as exc:
 
3292
  )
3293
  return ''
3294
 
3295
+ def _candidate_citation_tags(
3296
+ self, sentence, required_tags, limit, *, section_id='', section_name=''
3297
+ ):
3298
+ """Return source-role-eligible semantic-audit candidates for one claim.
3299
+
3300
+ S0-R1-C2 closes the remaining candidate-pool ownership gap: both the
3301
+ writer-supplied tag and lexical alternatives must satisfy the same section
3302
+ source-role contract before the semantic auditor is allowed to consider
3303
+ them. Lexical overlap only ranks eligible candidates; it never authorizes
3304
+ attachment by itself.
3305
+ """
3306
  valid_required = [
3307
  tag for tag in required_tags
3308
+ if tag in self.citation_map and self.citation_source_role_eligible(
3309
+ tag, section_id=section_id, section_name=section_name
3310
+ )
3311
  ]
3312
  sentence_terms = self._citation_terms(sentence)
3313
  scored = []
3314
  for tag, metadata in self.citation_map.items():
3315
  if tag in valid_required:
3316
  continue
3317
+ if not self.citation_source_role_eligible(
3318
+ tag, metadata=metadata, section_id=section_id, section_name=section_name
3319
+ ):
3320
+ continue
3321
  evidence = " ".join(
3322
  [
3323
  str(metadata.get("ref", "")),
 
4173
  self,
4174
  text,
4175
  section_name="",
4176
+ section_id="",
4177
  ):
4178
  """Audit citation support while granting the audit TAG-ONLY mutation authority.
4179
 
 
4217
  sentence,
4218
  required_tags,
4219
  candidate_limit,
4220
+ section_id=section_id, section_name=section_name,
4221
  )
4222
  evidence = []
4223
  for tag in candidates:
 
4258
  1. Preserve the sentence's substantive meaning, every number, percentage, coefficient, quotation, variable label, and empirical direction.
4259
  2. Every output sentence may contain at most one [CITE-XX] tag.
4260
  3. When several sources support the same claim, retain only the single source whose supplied evidence most directly supports that claim.
4261
+ 4. Use status=split only when no single candidate source directly supports the complete original sentence AND at least two distinct candidate sources are needed for genuinely different claims. If one candidate source directly supports the complete sentence, status must be ok and that single tag must be selected; do not split merely because the sentence has multiple clauses.
4262
  5. A source may be used only when its supplied title/abstract evidence directly supports the adjacent claim. Do not infer support from the reference title alone when the evidence is empty or unrelated.
4263
  6. Use only candidate tags supplied for that item. Never invent a source, fact, number, quotation, or citation tag.
4264
  7. If no candidate source directly supports a claim that requires external evidence, set status to unsupported and leave revised_text equal to the original sentence.
 
4390
  )
4391
  continue
4392
  if status == "split":
4393
+ # S0-R1-C2 decision-consistency postcondition. The audit
4394
+ # contract permits split only when different sources are
4395
+ # actually required. If the auditor itself returns exactly
4396
+ # one eligible tag, that is evidence that one source supports
4397
+ # the complete claim; keep the original sentence and bind that
4398
+ # one tag instead of consuming the Section retry.
4399
+ if len(revised_tags) == 1:
4400
+ selected = next(iter(revised_tags))
4401
+ replacements[record["id"]] = self._citation_tag_only_rewrite(
4402
+ record["original"], selected
4403
+ )
4404
+ self._append_citation_audit(
4405
+ section_name, "normalize_inconsistent_split_to_single_source",
4406
+ record["original"], replacements[record["id"]],
4407
+ reason or "Audit returned split with only one eligible supporting source.",
4408
+ )
4409
+ continue
4410
  violation = {
4411
  "type":"citation_claim_split_required", "id":record["id"],
4412
  "sentence":record["original"][:500], "reason":reason,
 
4609
  )
4610
  return revised
4611
 
4612
+ def audit_and_resolve_citations(self, text, section_name='', section_id='', quarantine=False):
4613
  self._last_citation_semantic_violations = []
4614
  value = str(text or '')
4615
  if quarantine:
 
4623
  violations = self.find_multi_citation_violations(value)
4624
  if has_citations and self.citation_semantic_audit.get('enabled', True):
4625
  try:
4626
+ value = self.semantic_audit_claim_specific_citations(
4627
+ value, section_name=section_name, section_id=section_id
4628
+ )
4629
  except PipelineCancelledError:
4630
  raise
4631
  except CitationAuditError as error: