S0-R1-C3 Quantitative Causal-Negation Semantic Guard

#76
Files changed (1) hide show
  1. core/quantitative_engine.py +64 -4
core/quantitative_engine.py CHANGED
@@ -493,6 +493,47 @@ Keywords: {keywords}"""
493
  ] if strict else [],
494
  }
495
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
496
  @classmethod
497
  def inspect_quantitative_study_fact_contract(cls, text, metadata, section_name=""):
498
  """Pure AMJ quantitative Evidence Ledger inspection; never rewrites prose."""
@@ -574,7 +615,10 @@ Keywords: {keywords}"""
574
  violations.append({"type":"construct_role_mismatch","role":role,"observed":candidate,"expected":expected,"preview":sentence.strip()[:180]})
575
  if strict:
576
  for pattern in causal_patterns:
577
- if re.search(pattern, sentence):
 
 
 
578
  violations.append({"type":"causal_language_violation","preview":sentence.strip()[:180]})
579
  break
580
  if (any(c in sentence.casefold() for c in construct_folded) if construct_folded else False) and re.search(r"(?i)\bp\s*(?:=|<|>|≤|≥)\s*(?:0?\.\d+|\d+(?:\.\d+)?)", sentence):
@@ -706,9 +750,25 @@ Keywords: {keywords}"""
706
 
707
  if strict:
708
  for pattern, replacement in causal_replacements:
709
- revised, count = re.subn(pattern, replacement, revised)
710
- if count:
711
- diagnostics.append({"type":"causal_language_repaired","replacement":replacement,"count":count})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
712
  if any(c in revised.casefold() for c in construct_folded) if construct_folded else False:
713
  revised, p_count = re.subn(r"(?i)\s*[,;]?\s*p\s*(?:=|<|>|≤|≥)\s*(?:0?\.\d+|\d+(?:\.\d+)?)", "", revised)
714
  if p_count:
 
493
  ] if strict else [],
494
  }
495
 
496
+ @staticmethod
497
+ def _causal_phrase_is_explicitly_disclaimed(sentence, match_start, match_end):
498
+ """Return True only when a causal phrase is explicitly scoped by a limitation.
499
+
500
+ This is intentionally local to the matched phrase rather than sentence-wide so a
501
+ sentence can safely say that causal identification is unavailable while still
502
+ failing closed if a separate clause makes an affirmative causal claim.
503
+ """
504
+ value = str(sentence or "")
505
+ left_floor = max(0, int(match_start) - 180)
506
+ left = value[left_floor:int(match_start)]
507
+ # Do not let a disclaimer in an earlier independent/contrast clause shield a later
508
+ # affirmative causal assertion. Keep the closest clause-like segment only.
509
+ boundaries = list(re.finditer(
510
+ r"(?i)(?:[;:,]|\bbut\b|\bhowever\b|\bwhereas\b|\byet\b|\bnevertheless\b)",
511
+ left,
512
+ ))
513
+ if boundaries:
514
+ left = left[boundaries[-1].end():]
515
+ left_folded = re.sub(r"\s+", " ", left.casefold()).strip()
516
+ right = value[int(match_end):min(len(value), int(match_end) + 140)]
517
+ right_folded = re.sub(r"\s+", " ", right.casefold()).strip()
518
+
519
+ pre_disclaimer_patterns = (
520
+ 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*)?$",
521
+ 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*)?$",
522
+ r"\b(?:unable|insufficient)\s+to\s+(?:identify|establish|demonstrate|show|infer|support|justify|warrant)\s*(?:(?:an?|the)\s*)?$",
523
+ r"\b(?:fails?|failed)\s+to\s+(?:identify|establish|demonstrate|show|infer|support|justify|warrant)\s*(?:(?:an?|the)\s*)?$",
524
+ r"\bno\s+(?:evidence|basis|support)\s+(?:for|of)\s*(?:(?:an?|the)\s*)?$",
525
+ r"\bnot\s+(?:evidence|proof)\s+(?:for|of)\s*(?:(?:an?|the)\s*)?$",
526
+ r"\bno\s*$",
527
+ )
528
+ if any(re.search(pattern, left_folded) for pattern in pre_disclaimer_patterns):
529
+ return True
530
+
531
+ post_disclaimer_patterns = (
532
+ r"^(?:\s*(?:is|are|was|were|has|have|had))?\s*(?:not|never)\s+(?:identified|established|demonstrated|shown|inferred|supported|claimed|justified|warranted)\b",
533
+ 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",
534
+ )
535
+ return any(re.search(pattern, right_folded) for pattern in post_disclaimer_patterns)
536
+
537
  @classmethod
538
  def inspect_quantitative_study_fact_contract(cls, text, metadata, section_name=""):
539
  """Pure AMJ quantitative Evidence Ledger inspection; never rewrites prose."""
 
615
  violations.append({"type":"construct_role_mismatch","role":role,"observed":candidate,"expected":expected,"preview":sentence.strip()[:180]})
616
  if strict:
617
  for pattern in causal_patterns:
618
+ match = re.search(pattern, sentence)
619
+ if match and not cls._causal_phrase_is_explicitly_disclaimed(
620
+ sentence, match.start(), match.end()
621
+ ):
622
  violations.append({"type":"causal_language_violation","preview":sentence.strip()[:180]})
623
  break
624
  if (any(c in sentence.casefold() for c in construct_folded) if construct_folded else False) and re.search(r"(?i)\bp\s*(?:=|<|>|≤|≥)\s*(?:0?\.\d+|\d+(?:\.\d+)?)", sentence):
 
750
 
751
  if strict:
752
  for pattern, replacement in causal_replacements:
753
+ regex = re.compile(pattern)
754
+ repaired_count = 0
755
+
756
+ def causal_repl(match):
757
+ nonlocal repaired_count
758
+ if cls._causal_phrase_is_explicitly_disclaimed(
759
+ revised, match.start(), match.end()
760
+ ):
761
+ return match.group(0)
762
+ repaired_count += 1
763
+ return replacement
764
+
765
+ revised = regex.sub(causal_repl, revised)
766
+ if repaired_count:
767
+ diagnostics.append({
768
+ "type":"causal_language_repaired",
769
+ "replacement":replacement,
770
+ "count":repaired_count,
771
+ })
772
  if any(c in revised.casefold() for c in construct_folded) if construct_folded else False:
773
  revised, p_count = re.subn(r"(?i)\s*[,;]?\s*p\s*(?:=|<|>|≤|≥)\s*(?:0?\.\d+|\d+(?:\.\d+)?)", "", revised)
774
  if p_count: