diff --git a/scripts/check_paradigm_shift.py b/scripts/check_paradigm_shift.py index 6440486d..68a05baf 100755 --- a/scripts/check_paradigm_shift.py +++ b/scripts/check_paradigm_shift.py @@ -28,6 +28,12 @@ to a negated clause followed by an It/They/You + to-be sentence; looser period-split parallelism without negation stays LLM-only) + E — " is not X. is Y" (restated subject, with or without + the/this/that; the same words must open both sentences) + + F — "is not just X. It may be / can become Y" (period-split with a + modal; lower precision, its own label so Ralph's LLM decides) + D — "Not X but Y" (Aristotelian inversion; informational) """ from __future__ import annotations @@ -87,6 +93,35 @@ "antithesis-restated-subject", "high", ), + ( + # Restated bare-noun subject at the start of a sentence: "Least + # privilege is not just fewer actions. Least privilege is smaller + # failure domains." Same backreference guard as above, for a subject + # of one to three words without the/this/that (2026-10-23 miss). + re.compile( + rf"(?im)(?:^|(?<=[.!?] ))(?:[-*]\s+)?" + rf"([a-z]\w*(?:\s+\w+){{0,2}})\s+(?:is|are|was|were)" + rf"(?:\s+not|n{_AP}?t)\s+[^.;:\n]{{1,80}}\.\s+" + rf"\1\s+(?:is|are|was|were)\b" + ), + "antithesis-restated-noun", + "high", + ), + ( + # Period-split with a modal or "become" in the second sentence: + # "... is not just a cluster detail. It can become a lateral movement + # primitive." Requires "not just" ("The build is not finished. It can + # be retried." is ordinary prose). Lower precision than the forms + # above (a regex cannot tell whether "It" refers to the rejected + # subject), so the label is kept separate and Ralph's LLM pass decides. + re.compile( + rf"(?i)\b(?:is|are|was|were)(?:\s+not|n{_AP}?t)\s+just\s+" + rf"[^.;:\n]{{1,80}}\.\s+(?:It|They|This)\s+" + rf"(?:(?:may|might|can|could)\s+(?:also\s+)?(?:be|become)|becomes?)\b" + ), + "antithesis-period-split-modal", + "high", + ), ( re.compile( r"(?i)\bnot\s+[a-z]\w+(?:ing|ed|s|ly)?[,.\s]+but\s+[a-z]\w+" diff --git a/tests/test_check_paradigm_shift.py b/tests/test_check_paradigm_shift.py index 7fcd3871..a3f26c5c 100644 --- a/tests/test_check_paradigm_shift.py +++ b/tests/test_check_paradigm_shift.py @@ -135,6 +135,68 @@ def test_negative_no_negation(self): self.assertNotIn("antithesis-restated-subject", labels_for(text)) +class TestRestatedNoun(unittest.TestCase): + """' is not X. is Y.' without the/this/that (2026-10-23 miss).""" + + def test_bare_noun_not_just(self): + # iam-blast-radius-architecture-problem L28 + text = "Least privilege is not just fewer actions. Least privilege is smaller failure domains." + self.assertIn("antithesis-restated-noun", labels_for(text)) + + def test_mid_paragraph_and_contraction(self): + text = "We tried. Defense in depth isn’t a product. Defense in depth is a habit." + self.assertIn("antithesis-restated-noun", labels_for(text)) + + def test_bullet(self): + text = "- Logging is not a control. Logging is evidence." + self.assertIn("antithesis-restated-noun", labels_for(text)) + + def test_determiner_form_reported_once(self): + text = "The goal is not a prettier IAM policy. The goal is a smaller blast radius." + self.assertEqual(labels_for(text), ["antithesis-restated-subject"]) + + def test_negative_different_subject(self): + text = "Logging is not enabled. Monitoring is on." + self.assertNotIn("antithesis-restated-noun", labels_for(text)) + + def test_negative_subject_mid_sentence(self): + # the repeated words do not open the first sentence + text = "We know least privilege is not free. Least privilege is still worth it." + self.assertNotIn("antithesis-restated-noun", labels_for(text)) + + +class TestPeriodSplitModal(unittest.TestCase): + """'... is not just X. It may be / can become Y.' (2026-10-23 miss).""" + + def test_it_may_be(self): + # iam-blast-radius-architecture-problem L32 + text = ( + "A workload identity that can read secrets is not just an " + "application permission. It may be a persistence path." + ) + self.assertIn("antithesis-period-split-modal", labels_for(text)) + + def test_it_can_become(self): + # iam-blast-radius-architecture-problem L33 + text = ( + "A service account that can exec into pods is not just a cluster " + "detail. It can become a lateral movement primitive." + ) + self.assertIn("antithesis-period-split-modal", labels_for(text)) + + def test_it_becomes(self): + text = "The runner is not just a build box. It becomes a deploy credential." + self.assertIn("antithesis-period-split-modal", labels_for(text)) + + def test_negative_without_just(self): + text = "The build is not finished. It can be retried after the fix." + self.assertEqual(labels_for(text), []) + + def test_negative_will_be(self): + text = "The report is not just late. It will be rewritten." + self.assertNotIn("antithesis-period-split-modal", labels_for(text)) + + class TestExistingPatternsStillWork(unittest.TestCase): def test_pronoun_period_split_iam_blast(self): # iam-blast-radius-github-action L16 (2026-10-23): flagged here, but the