S0-R1-C5.5-R3 Typed Causal Actor Frame Closure

#86
Files changed (1) hide show
  1. core/quantitative_engine.py +91 -45
core/quantitative_engine.py CHANGED
@@ -878,53 +878,76 @@ Keywords: {keywords}"""
878
  r"causation|cause(?:s|d|ing)?)"
879
  )
880
  direct_patterns = (
881
- r"\b(?:causes?|caused|causing)\b",
882
- r"\b(?:leads?|led)\s+to\b",
883
- r"\b(?:drives?|drove|driven)\b",
884
- r"\b(?:determines?|determined)\b",
885
- r"\b(?:produces?|produced)\b",
886
- r"\b(?:results?|resulted)\s+in\b",
887
- r"\b(?:transmits?|transmitted)\s+(?:an?\s+|the\s+)?causal\s+effect\b",
888
- r"\bcausally\s+(?:transmits?|mediates?|affects?|influences?)\b",
889
- r"\b(?:has|have|had|exerts?|exerted)\s+(?:an?\s+|the\s+)?causal\s+effect\b",
 
 
890
  )
891
 
892
- def direct_match_is_statistical_reporting(match):
893
- """Return True only for a statistical-operation -> statistical-artifact frame.
894
-
895
- C5.5-R1: causal-looking verbs such as ``produced``/``resulted in``
896
- are ambiguous in quantitative prose. A bootstrap procedure producing
897
- a confidence interval is statistical reporting, not a claim that one
898
- study construct causes another. The exemption therefore requires
899
- *both* a statistical-operation source immediately before the verb and
900
- a statistical-artifact target immediately after it. Merely seeing
901
- ``analysis`` or ``model`` is not enough; e.g. ``the analysis produced
902
- higher engagement`` remains fail-closed.
903
  """
904
- prefix = value[max(0, match.start() - 180):match.start()]
905
- suffix = value[match.end():min(len(value), match.end() + 180)]
906
-
907
- # Keep the source local to the current grammatical frame rather than
908
- # letting an earlier sentence mention of an analysis whitelist a later
909
- # construct-level causal assertion.
910
- source = re.split(r"[.;!?]", prefix)[-1]
911
- source_pattern = (
912
- r"\b(?:bootstrap(?:\s+resampling|\s+procedure|\s+computation)?|"
913
- r"resampling(?:\s+procedure|\s+algorithm)?|"
914
- r"regression\s+(?:analysis|model)|"
915
- r"association\s+(?:analysis|model)|"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
916
  r"mediation\s+(?:analysis|model)|"
917
- r"estimation(?:\s+procedure|\s+method)?|estimator|"
918
- r"statistical\s+(?:analysis|model|procedure|method)|"
919
- r"analysis|analyses|model|procedure|method|algorithm|"
920
- r"computation|calculation)\b"
 
 
 
 
 
 
 
921
  )
922
- if not re.search(source_pattern, source, flags=re.I):
923
- return False
924
-
925
- # The target must be a recognisable statistical artefact. Do not use
926
- # generic outcomes such as ``result`` or ``change`` because those can
927
- # also be substantive dependent variables.
 
 
928
  target_pattern = (
929
  r"^\s*(?:(?:an?|the|this|that|these|those|following|reported|"
930
  r"standardized|standardised|numerical|estimated|95\s*percent|95%)\s+){0,6}"
@@ -935,10 +958,33 @@ Keywords: {keywords}"""
935
  r"correlations?|covariances?|variances?|loadings?|"
936
  r"test\s+statistics?|p[- ]?values?|vif(?:s|\s+values)?|"
937
  r"cronbach(?:'s)?\s+alpha|composite\s+reliability|"
938
- r"average\s+variance\s+extracted|ave|numerical\s+estimates?|estimates?)\b"
 
 
 
939
  )
940
  return bool(re.search(target_pattern, suffix, flags=re.I))
941
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
942
  def direct_match_is_disclaimed(match):
943
  prefix = value[max(0, match.start() - 220):match.start()]
944
  if re.search(
@@ -974,12 +1020,12 @@ Keywords: {keywords}"""
974
  return False
975
 
976
  saw_disclaimed_direct = False
977
- for pattern in direct_patterns:
978
  for match in re.finditer(pattern, value, flags=re.I):
979
  if direct_match_is_disclaimed(match):
980
  saw_disclaimed_direct = True
981
  continue
982
- if direct_match_is_statistical_reporting(match):
983
  continue
984
  return "AFFIRMATIVE_CAUSAL"
985
 
 
878
  r"causation|cause(?:s|d|ing)?)"
879
  )
880
  direct_patterns = (
881
+ (r"\b(?:causes?|caused|causing)\b", "STRONG"),
882
+ (r"\b(?:leads?|led)\s+to\b", "STRONG"),
883
+ (r"\b(?:drives?|drove|driven)\b", "STRONG"),
884
+ (r"\b(?:determines?|determined)\b", "STRONG"),
885
+ (r"\b(?:produces?|produced)\b", "AMBIGUOUS_REPORTING"),
886
+ (r"\b(?:results?|resulted)\s+in\b", "AMBIGUOUS_REPORTING"),
887
+ (r"\b(?:yields?|yielded)\b", "AMBIGUOUS_REPORTING"),
888
+ (r"\b(?:generates?|generated)\b", "AMBIGUOUS_REPORTING"),
889
+ (r"\b(?:transmits?|transmitted)\s+(?:an?\s+|the\s+)?causal\s+effect\b", "STRONG"),
890
+ (r"\bcausally\s+(?:transmits?|mediates?|affects?|influences?)\b", "STRONG"),
891
+ (r"\b(?:has|have|had|exerts?|exerted)\s+(?:an?\s+|the\s+)?causal\s+effect\b", "STRONG"),
892
  )
893
 
894
+ def local_actor_text(match):
895
+ """Return the grammatical actor window immediately before a predicate.
896
+
897
+ R3 deliberately classifies the *actor* rather than maintaining an
898
+ ever-growing whitelist of reporting artefacts. Leading adverbial or
899
+ prepositional phrases (``In the analysis, ...`` / ``Using Spearman
900
+ correlations, ...``) are peeled away so that a nearby mention of an
901
+ analysis cannot shield a later construct-level causal assertion.
 
 
 
902
  """
903
+ prefix = value[max(0, match.start() - 260):match.start()]
904
+ actor = re.split(r"[.;!?]", prefix)[-1].strip()
905
+ lead = re.match(
906
+ r"^(?:in|using|with|based\s+on|according\s+to|after|before|when|while|because|"
907
+ r"although|though|given|under|for|as)\b[^,]{0,180},\s*(.+)$",
908
+ actor,
909
+ flags=re.I,
910
+ )
911
+ if lead:
912
+ actor = lead.group(1).strip()
913
+ return actor[-220:]
914
+
915
+ def actor_type(match):
916
+ """Classify the predicate subject as analytical, weak-analytical, or other.
917
+
918
+ ``ANALYTICAL_ACTOR`` is intentionally restricted to explicit
919
+ statistical/diagnostic actors. Generic ``model``/``procedure`` terms
920
+ are weak because they can also denote the substantive AI system or an
921
+ intervention; they are exempt only when the predicate target is a
922
+ recognisable reporting artefact.
923
+ """
924
+ actor = local_actor_text(match)
925
+ strong_actor = (
926
+ r"\b(?:analysis|analyses|"
927
+ r"bootstrap(?:\s+(?:resampling|procedure|computation|analysis))?|"
928
+ r"resampling(?:\s+(?:procedure|analysis|computation))?|"
929
+ r"regression\s+(?:analysis|model)|association\s+(?:analysis|model)|"
930
  r"mediation\s+(?:analysis|model)|"
931
+ r"estimation(?:\s+(?:procedure|method|analysis))?|estimator|"
932
+ r"statistical\s+(?:analysis|model|procedure|method|test)|"
933
+ r"robustness\s+(?:check|test|analysis)|"
934
+ r"sensitivity\s+(?:check|test|analysis)|"
935
+ r"diagnostic\s+(?:check|test|analysis)|"
936
+ r"rank[- ]order\s+(?:robustness\s+)?check|"
937
+ r"(?:spearman|pearson|rank[- ]order)\s+correlations?|"
938
+ r"correlation\s+(?:analysis|matrix|check|test)|"
939
+ r"hypothesis\s+test|significance\s+test|"
940
+ r"variance\s+inflation\s+factor(?:\s+check)?|vif(?:\s+check)?|"
941
+ r"measurement\s+assessment|descriptive\s+analysis)\b"
942
  )
943
+ if re.search(strong_actor, actor, flags=re.I):
944
+ return "ANALYTICAL_ACTOR"
945
+ if re.search(r"\b(?:model|procedure|method|computation|calculation)\b", actor, flags=re.I):
946
+ return "WEAK_ANALYTICAL_ACTOR"
947
+ return "OTHER_ACTOR"
948
+
949
+ def predicate_target_is_reporting_output(match):
950
+ suffix = value[match.end():min(len(value), match.end() + 220)]
951
  target_pattern = (
952
  r"^\s*(?:(?:an?|the|this|that|these|those|following|reported|"
953
  r"standardized|standardised|numerical|estimated|95\s*percent|95%)\s+){0,6}"
 
958
  r"correlations?|covariances?|variances?|loadings?|"
959
  r"test\s+statistics?|p[- ]?values?|vif(?:s|\s+values)?|"
960
  r"cronbach(?:'s)?\s+alpha|composite\s+reliability|"
961
+ r"average\s+variance\s+extracted|ave|numerical\s+estimates?|estimates?|"
962
+ r"(?:a\s+)?pattern(?:\s+of\s+(?:associations|correlations|estimates))?|"
963
+ r"(?:a\s+)?(?:similar|consistent|comparable)\s+pattern|"
964
+ r"findings?|results?)\b"
965
  )
966
  return bool(re.search(target_pattern, suffix, flags=re.I))
967
 
968
+ def direct_match_is_analytical_reporting(match, predicate_kind):
969
+ actor = actor_type(match)
970
+ if predicate_kind == "AMBIGUOUS_REPORTING":
971
+ # ``analysis/check/test produced/resulted/yielded/generated ...``
972
+ # describes what an analytical operation reported. It is not a
973
+ # study-construct causal assertion even when the following prose is
974
+ # stylistically loose (e.g. ``the analysis produced higher
975
+ # engagement``). The CAR layer may flag wording separately; this
976
+ # causal gate must not terminate the manuscript for it.
977
+ if actor == "ANALYTICAL_ACTOR":
978
+ return True
979
+ if actor == "WEAK_ANALYTICAL_ACTOR" and predicate_target_is_reporting_output(match):
980
+ return True
981
+ return False
982
+
983
+ # Strong causal predicates remain fail-closed. Preserve only the
984
+ # narrow statistical-reporting idiom already accepted by R1, such as
985
+ # ``the analysis led to the following numerical estimate``.
986
+ return actor in {"ANALYTICAL_ACTOR", "WEAK_ANALYTICAL_ACTOR"} and predicate_target_is_reporting_output(match)
987
+
988
  def direct_match_is_disclaimed(match):
989
  prefix = value[max(0, match.start() - 220):match.start()]
990
  if re.search(
 
1020
  return False
1021
 
1022
  saw_disclaimed_direct = False
1023
+ for pattern, predicate_kind in direct_patterns:
1024
  for match in re.finditer(pattern, value, flags=re.I):
1025
  if direct_match_is_disclaimed(match):
1026
  saw_disclaimed_direct = True
1027
  continue
1028
+ if direct_match_is_analytical_reporting(match, predicate_kind):
1029
  continue
1030
  return "AFFIRMATIVE_CAUSAL"
1031