S0-R1-C5.4 Contract Consistency Failure Surface Stabilization

#82
core/generation_runner.py CHANGED
@@ -944,6 +944,13 @@ class GenerationRunner:
944
  continue
945
  kind = str(item.get('type') or 'validation_issue')
946
  detail = str(item.get('repair_instruction') or item.get('reason') or item.get('preview') or '')
 
 
 
 
 
 
 
947
  actor = str(item.get('actor') or '')
948
  expected = item.get('expected')
949
  observed = item.get('observed')
@@ -3343,6 +3350,16 @@ class GenerationRunner:
3343
  "\nASQ EVIDENCE-CHANNEL RULE: A sentence whose factual subject is this study's setting, participants, interviews, data sources, cases, actors, observations, events, quotations, analytic steps, or empirical findings must not carry an external [CITE-XX] tag. "
3344
  "Place any prior-literature or methodological-rationale claim in a separate sentence and cite only that external claim."
3345
  )
 
 
 
 
 
 
 
 
 
 
3346
  memory_context = agent._build_memory_context(sec_title, 2500)
3347
  memory_block = f"\n<GLOBAL_MEMORY>\n{memory_context}\n</GLOBAL_MEMORY>\n" if memory_context else ""
3348
  snippets = rag_engine.get_relevant_context(display_name) if rag_engine is not None else ""
@@ -3458,6 +3475,7 @@ Planning length: approximately {int(adjusted_target * min_ratio)}-{int(adjusted_
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}
 
944
  continue
945
  kind = str(item.get('type') or 'validation_issue')
946
  detail = str(item.get('repair_instruction') or item.get('reason') or item.get('preview') or '')
947
+ if kind == 'causal_language_violation' and not detail:
948
+ detail = (
949
+ "Rewrite only the causal-strength wording. Preserve the study fact and analytical meaning, "
950
+ "but state association/covariation rather than causal identification. Explicit limitation "
951
+ "phrases such as 'restricted to associations, not causal effects' and 'cannot establish "
952
+ "causality' are valid and should not be weakened further."
953
+ )
954
  actor = str(item.get('actor') or '')
955
  expected = item.get('expected')
956
  observed = item.get('observed')
 
3350
  "\nASQ EVIDENCE-CHANNEL RULE: A sentence whose factual subject is this study's setting, participants, interviews, data sources, cases, actors, observations, events, quotations, analytic steps, or empirical findings must not carry an external [CITE-XX] tag. "
3351
  "Place any prior-literature or methodological-rationale claim in a separate sentence and cite only that external claim."
3352
  )
3353
+ if section_id not in {"results", "findings", "appendix"}:
3354
+ citation_rule += (
3355
+ "\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. "
3356
+ "If prior literature provides a rationale, state that external rationale in a separate sentence and cite only that sentence."
3357
+ )
3358
+ if bool(state.get("evidence_risk_detected")) and section_id not in {"results", "findings", "appendix"}:
3359
+ citation_rule += (
3360
+ "\nEVIDENCE-LIMITED CLAIM-DENSITY CONTRACT: Direct high-relevance literature coverage is limited. Treat assigned claim-evidence items as the ceiling for source-dependent empirical, definitional, prevalence, and field-gap claims in this unit. "
3361
+ "Do not invent additional literature claims merely to make the section sound comprehensive. Prefer concise study positioning, cautious conceptual reasoning, and clearly qualified interpretations when no directly validated source is assigned."
3362
+ )
3363
  memory_context = agent._build_memory_context(sec_title, 2500)
3364
  memory_block = f"\n<GLOBAL_MEMORY>\n{memory_context}\n</GLOBAL_MEMORY>\n" if memory_context else ""
3365
  snippets = rag_engine.get_relevant_context(display_name) if rag_engine is not None else ""
 
3475
  {citation_rule}
3476
  {table_rule}
3477
  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.
3478
+ {("CAUSAL-STRENGTH CONTRACT: This cross-sectional observational design supports association/covariation language only. Do not state that one construct causes, leads to, drives, determines, produces, or causally mediates another. Explicit limitation language is valid and preferred when relevant, including 'restricted to associations, not causal effects', 'cannot establish causality', and 'the indirect association is not causal mediation identification'." if mode == "amj" else "")}
3479
  {self._hypothesis_contract_directive(hypothesis_ledger)}
3480
  {str(sec.get('required_content') or '')}
3481
  {qualitative_rule}
core/pipeline_agent.py CHANGED
@@ -2774,6 +2774,13 @@ INPUT:
2774
  or section_loads.get(section, 0) >= capacities.get(section, target)
2775
  ):
2776
  continue
 
 
 
 
 
 
 
2777
  evidence = str((self.citation_map.get(tag) or {}).get('evidence') or '')
2778
  if len(self._citation_terms(claim) & self._citation_terms(evidence)) < 2:
2779
  continue
@@ -3256,10 +3263,20 @@ INPUT:
3256
  section = str(section_name or '').lower()
3257
  if not text:
3258
  return 'STRUCTURAL'
3259
- if re.match(r'^(?:h\d+[a-z]?\s*[:.\-]|hypothesis\s+\d+\b)', lower):
3260
  return 'HYPOTHESIS_STATEMENT'
3261
  if re.search(r'\b(?:we hypothesi[sz]e|we propose|we predict|we expect)\b', lower):
3262
  return 'HYPOTHESIS_STATEMENT'
 
 
 
 
 
 
 
 
 
 
3263
  if re.search(
3264
  r'\b(?:the present study|this study|our study|the current study|the present research|this research)\b',
3265
  lower,
 
2774
  or section_loads.get(section, 0) >= capacities.get(section, target)
2775
  ):
2776
  continue
2777
+ provenance = self._citation_claim_provenance(claim, section)
2778
+ if provenance != 'EXTERNAL_LITERATURE_CLAIM':
2779
+ logging.info(
2780
+ 'Citation planner rejected non-literature claim provenance: section=%s tag=%s provenance=%s',
2781
+ section, tag, provenance,
2782
+ )
2783
+ continue
2784
  evidence = str((self.citation_map.get(tag) or {}).get('evidence') or '')
2785
  if len(self._citation_terms(claim) & self._citation_terms(evidence)) < 2:
2786
  continue
 
3263
  section = str(section_name or '').lower()
3264
  if not text:
3265
  return 'STRUCTURAL'
3266
+ if re.match(r'^(?:h\d+[a-z]?\b|hypothesis\s+\d+\b)', lower):
3267
  return 'HYPOTHESIS_STATEMENT'
3268
  if re.search(r'\b(?:we hypothesi[sz]e|we propose|we predict|we expect)\b', lower):
3269
  return 'HYPOTHESIS_STATEMENT'
3270
+ if re.search(
3271
+ r'\b(?:the research question|our research question|research question guiding the study|the focal research question)\b',
3272
+ lower,
3273
+ ):
3274
+ return 'STUDY_POSITIONING'
3275
+ if re.search(
3276
+ r'\b(?:h\d+[a-z]?\s+(?:states?|proposes?|predicts?|addresses?|posits?|hypothesi[sz]es?|tests?))\b',
3277
+ lower,
3278
+ ):
3279
+ return 'HYPOTHESIS_STATEMENT'
3280
  if re.search(
3281
  r'\b(?:the present study|this study|our study|the current study|the present research|this research)\b',
3282
  lower,
core/quantitative_engine.py CHANGED
@@ -600,51 +600,77 @@ Keywords: {keywords}"""
600
  ] if strict else [],
601
  }
602
 
603
- @staticmethod
604
- def _causal_phrase_is_explicitly_disclaimed(sentence, match_start, match_end):
605
- """Return True only when a causal phrase is explicitly scoped by a limitation.
606
 
607
- This is intentionally local to the matched phrase rather than sentence-wide so a
608
- sentence can safely say that causal identification is unavailable while still
609
- failing closed if a separate clause makes an affirmative causal claim.
 
610
  """
611
- value = str(sentence or "")
612
- left_floor = max(0, int(match_start) - 180)
613
- left = value[left_floor:int(match_start)]
614
- # Do not let a disclaimer in an earlier independent/contrast clause shield a later
615
- # affirmative causal assertion. Keep the closest clause-like segment only.
616
- boundaries = list(re.finditer(
617
- r"(?i)(?:[;:,]|\bbut\b|\bhowever\b|\bwhereas\b|\byet\b|\bnevertheless\b)",
618
- left,
619
- ))
620
- if boundaries:
621
- left = left[boundaries[-1].end():]
622
- left_folded = re.sub(r"\s+", " ", left.casefold()).strip()
623
- right = value[int(match_end):min(len(value), int(match_end) + 140)]
624
- right_folded = re.sub(r"\s+", " ", right.casefold()).strip()
625
-
626
- pre_disclaimer_patterns = (
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*)?$",
637
- r"\bno\s*$",
638
  )
639
- if any(re.search(pattern, left_folded) for pattern in pre_disclaimer_patterns):
640
- return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
641
 
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
 
649
  @classmethod
650
  def inspect_quantitative_study_fact_contract(cls, text, metadata, section_name=""):
@@ -668,15 +694,6 @@ Keywords: {keywords}"""
668
  "mediator": r"(?:mediator|mediating\s+variable|mediating\s+construct)",
669
  "outcome": r"(?:outcome|dependent\s+variable|outcome\s+construct)",
670
  }
671
- causal_patterns = (
672
- r"(?i)\bhad\s+a\s+causal\s+effect\s+on\b",
673
- r"(?i)\bexerted\s+a\s+causal\s+effect\s+on\b",
674
- r"(?i)\bcausal\s+effect\s+on\b",
675
- r"(?i)\b(?:an|a|the)\s+causal\s+effect\b",
676
- r"(?i)\bcausal\s+effects\b",
677
- r"(?i)\bcausal\s+effect\b",
678
- r"(?i)\bcausal\s+mediation\b",
679
- )
680
  violations = []
681
 
682
  def add_metric_violations(sentence, pattern, expected, *, dtype="float", label="metric"):
@@ -726,13 +743,18 @@ Keywords: {keywords}"""
726
  if pattern.search(sentence):
727
  violations.append({"type":"construct_role_mismatch","role":role,"observed":candidate,"expected":expected,"preview":sentence.strip()[:180]})
728
  if strict:
729
- for pattern in causal_patterns:
730
- match = re.search(pattern, sentence)
731
- if match and not cls._causal_phrase_is_explicitly_disclaimed(
732
- sentence, match.start(), match.end()
733
- ):
734
- violations.append({"type":"causal_language_violation","preview":sentence.strip()[:180]})
735
- break
 
 
 
 
 
736
  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):
737
  violations.append({"type":"unfrozen_p_value","preview":sentence.strip()[:180]})
738
  if ownership == "study":
@@ -861,26 +883,17 @@ Keywords: {keywords}"""
861
  diagnostics.append({"type":"construct_role_repaired","role":role,"observed":candidate,"expected":expected,"count":count})
862
 
863
  if strict:
864
- for pattern, replacement in causal_replacements:
865
- regex = re.compile(pattern)
866
- repaired_count = 0
867
-
868
- def causal_repl(match):
869
- nonlocal repaired_count
870
- if cls._causal_phrase_is_explicitly_disclaimed(
871
- revised, match.start(), match.end()
872
- ):
873
- return match.group(0)
874
- repaired_count += 1
875
- return replacement
876
-
877
- revised = regex.sub(causal_repl, revised)
878
- if repaired_count:
879
- diagnostics.append({
880
- "type":"causal_language_repaired",
881
- "replacement":replacement,
882
- "count":repaired_count,
883
- })
884
  if any(c in revised.casefold() for c in construct_folded) if construct_folded else False:
885
  revised, p_count = re.subn(r"(?i)\s*[,;]?\s*p\s*(?:=|<|>|≤|≥)\s*(?:0?\.\d+|\d+(?:\.\d+)?)", "", revised)
886
  if p_count:
 
600
  ] if strict else [],
601
  }
602
 
603
+ @classmethod
604
+ def _classify_quantitative_causal_clause(cls, clause):
605
+ """Classify study/result prose as causal assertion, explicit limitation, or non-causal.
606
 
607
+ The contract is semantic at the clause level rather than a blacklist of the
608
+ token ``causal``. Explicit statements that restrict interpretation to
609
+ association/covariation are permitted, while affirmative causal verbs and
610
+ causal-identification claims remain fail-closed.
611
  """
612
+ value = re.sub(r"\s+", " ", str(clause or "")).strip().casefold()
613
+ if not value:
614
+ return "NON_CAUSAL"
615
+
616
+ causal_nouns = r"(?:causal(?:ity|\s+(?:effect|effects|mediation|mechanism|pathway|relationship|relationships|identification))|cause(?:s|d)?|causation)"
617
+ limitation_patterns = (
618
+ rf"\b(?:cannot|can't|could not|couldn't|does not|doesn't|do not|don't|did not|didn't|fails? to|failed to|is not able to|are not able to|was not able to|were not able to|unable to|insufficient to)\s+(?:identify|establish|demonstrate|show|infer|support|imply|claim|justify|warrant|permit|allow|prove)\b[^.;]{{0,80}}\b{causal_nouns}\b",
619
+ rf"\b(?:is|are|was|were)\s+insufficient\s+to\s+(?:identify|establish|demonstrate|show|infer|support|prove)\b[^.;]{{0,80}}\b{causal_nouns}\b",
620
+ rf"\b(?:is|are|was|were)\s+not\s+(?:designed|intended)\s+to\s+(?:identify|establish|demonstrate|show|infer|support|prove)\b[^.;]{{0,80}}\b{causal_nouns}\b",
621
+ rf"\bwithout\s+(?:identifying|establishing|demonstrating|showing|inferring|supporting|proving)\b[^.;]{{0,80}}\b{causal_nouns}\b",
622
+ rf"\b(?:no|without)\s+(?:evidence|basis|support|identification|proof)\b[^.;]{{0,60}}\b{causal_nouns}\b",
623
+ rf"\b(?:not|never)\s+(?:evidence|proof)\s+(?:of|for)\b[^.;]{{0,50}}\b{causal_nouns}\b",
624
+ rf"\b(?:should not|shouldn't|must not|cannot|can't|could not|couldn't)\s+be\s+(?:interpreted|read|treated|understood)\s+as\b[^.;]{{0,60}}\b{causal_nouns}\b",
625
+ rf"\b(?:is|are|was|were)\s+(?:restricted|limited|confined)\s+to\b[^.;]{{0,120}}\b(?:not|rather than|instead of)\s+(?:to\s+)?\b{causal_nouns}\b",
626
+ rf"\b(?:restricted|limited|confined)\s+to\b[^.;]{{0,120}}\b(?:not|rather than|instead of)\s+(?:to\s+)?\b{causal_nouns}\b",
627
+ rf"\b(?:association|associations|covariation|covariance|correlation|correlations|indirect association|descriptive pattern|descriptive patterns)\b[^.;]{{0,80}}\b(?:not(?:\s+as)?|rather than|instead of)\s+(?:an?\s+|the\s+)?{causal_nouns}\b",
628
+ rf"\b{causal_nouns}\b\s+(?:is|are|was|were)\s+(?:not|never)\s+(?:identified|established|demonstrated|shown|inferred|supported|claimed|justified|warranted|proven)\b",
629
+ rf"\b{causal_nouns}\b\s+(?:cannot|can't|could not|couldn't)\s+be\s+(?:identified|established|demonstrated|shown|inferred|supported|claimed|justified|warranted|proven)\b",
630
+ rf"\b(?:inability|lack\s+of\s+(?:an?\s+)?ability)\s+to\s+(?:identify|establish|demonstrate|show|infer|support|justify|warrant|prove)\b[^.;]{{0,80}}\b{causal_nouns}\b",
631
+ rf"\b(?:not|no)\s+(?:an?\s+|the\s+)?{causal_nouns}\b",
632
+ rf"\b(?:cannot|can't|could not|couldn't)\s+be\s+used\s+to\s+(?:infer|establish|demonstrate|show|prove)\s+(?:an?\s+|the\s+)?{causal_nouns}\b",
633
+ rf"\bwithout\s+(?:proving|establishing|demonstrating|showing|identifying)\s+(?:an?\s+|the\s+)?{causal_nouns}\b",
 
 
 
 
 
634
  )
635
+ if any(re.search(pattern, value, flags=re.I) for pattern in limitation_patterns):
636
+ return "EXPLICIT_CAUSAL_LIMITATION"
637
+
638
+ affirmative_patterns = (
639
+ r"\b(?:causes?|caused|causing)\b",
640
+ r"\b(?:leads?|led)\s+to\b",
641
+ r"\b(?:drives?|drove|driven)\b",
642
+ r"\b(?:determines?|determined)\b",
643
+ r"\b(?:produces?|produced)\b",
644
+ r"\b(?:results?|resulted)\s+in\b",
645
+ r"\b(?:transmits?|transmitted)\s+(?:an?\s+|the\s+)?causal\s+effect\b",
646
+ r"\b(?:has|have|had|exerts?|exerted)\s+(?:an?\s+|the\s+)?causal\s+effect\b",
647
+ r"\bcausal\s+(?:effect|effects|mediation|mechanism|pathway|relationship|relationships|identification)\b",
648
+ r"\b(?:demonstrates?|demonstrated|shows?|showed|establishes?|established|identifies?|identified|proves?|proved)\s+(?:a\s+|the\s+)?causal(?:ity|\s+(?:effect|effects|mechanism|pathway|relationship|relationships|mediation))\b",
649
+ )
650
+ if any(re.search(pattern, value, flags=re.I) for pattern in affirmative_patterns):
651
+ return "AFFIRMATIVE_CAUSAL"
652
+ return "NON_CAUSAL"
653
 
654
+ @classmethod
655
+ def classify_quantitative_causal_sentence(cls, sentence):
656
+ """Three-state causal contract shared by inspection, repair, and tests."""
657
+ value = str(sentence or "")
658
+ # Contrast clauses must not let an early disclaimer shield a later assertion.
659
+ clauses = re.split(
660
+ r"(?i)(?:;|\bbut\b|\bhowever\b|\bwhereas\b|\byet\b|\bnevertheless\b|\balthough\b|\bthough\b)",
661
+ value,
662
  )
663
+ states = [cls._classify_quantitative_causal_clause(part) for part in clauses if str(part).strip()]
664
+ if "AFFIRMATIVE_CAUSAL" in states:
665
+ return "AFFIRMATIVE_CAUSAL"
666
+ if "EXPLICIT_CAUSAL_LIMITATION" in states:
667
+ return "EXPLICIT_CAUSAL_LIMITATION"
668
+ return "NON_CAUSAL"
669
+
670
+ @classmethod
671
+ def _causal_phrase_is_explicitly_disclaimed(cls, sentence, match_start=None, match_end=None):
672
+ """Compatibility wrapper for older call sites; uses the shared tri-state contract."""
673
+ return cls.classify_quantitative_causal_sentence(sentence) == "EXPLICIT_CAUSAL_LIMITATION"
674
 
675
  @classmethod
676
  def inspect_quantitative_study_fact_contract(cls, text, metadata, section_name=""):
 
694
  "mediator": r"(?:mediator|mediating\s+variable|mediating\s+construct)",
695
  "outcome": r"(?:outcome|dependent\s+variable|outcome\s+construct)",
696
  }
 
 
 
 
 
 
 
 
 
697
  violations = []
698
 
699
  def add_metric_violations(sentence, pattern, expected, *, dtype="float", label="metric"):
 
743
  if pattern.search(sentence):
744
  violations.append({"type":"construct_role_mismatch","role":role,"observed":candidate,"expected":expected,"preview":sentence.strip()[:180]})
745
  if strict:
746
+ causal_state = cls.classify_quantitative_causal_sentence(sentence)
747
+ if causal_state == "AFFIRMATIVE_CAUSAL":
748
+ violations.append({
749
+ "type":"causal_language_violation",
750
+ "preview":sentence.strip()[:180],
751
+ "reason":"The sentence makes an affirmative causal claim that exceeds the cross-sectional observational evidence contract.",
752
+ "repair_instruction":(
753
+ "Preserve the study meaning but rewrite as association/covariation only. "
754
+ "Explicit causal-limit language is allowed, including forms such as "
755
+ "'restricted to associations, not causal effects' or 'cannot establish causality'."
756
+ ),
757
+ })
758
  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):
759
  violations.append({"type":"unfrozen_p_value","preview":sentence.strip()[:180]})
760
  if ownership == "study":
 
883
  diagnostics.append({"type":"construct_role_repaired","role":role,"observed":candidate,"expected":expected,"count":count})
884
 
885
  if strict:
886
+ causal_state = cls.classify_quantitative_causal_sentence(revised)
887
+ if causal_state == "AFFIRMATIVE_CAUSAL":
888
+ for pattern, replacement in causal_replacements:
889
+ regex = re.compile(pattern)
890
+ revised, repaired_count = regex.subn(replacement, revised)
891
+ if repaired_count:
892
+ diagnostics.append({
893
+ "type":"causal_language_repaired",
894
+ "replacement":replacement,
895
+ "count":repaired_count,
896
+ })
 
 
 
 
 
 
 
 
 
897
  if any(c in revised.casefold() for c in construct_folded) if construct_folded else False:
898
  revised, p_count = re.subn(r"(?i)\s*[,;]?\s*p\s*(?:=|<|>|≤|≥)\s*(?:0?\.\d+|\d+(?:\.\d+)?)", "", revised)
899
  if p_count: