diff --git a/scripts/check_paradigm_shift.py b/scripts/check_paradigm_shift.py index c87156d1..6440486d 100755 --- a/scripts/check_paradigm_shift.py +++ b/scripts/check_paradigm_shift.py @@ -71,6 +71,22 @@ "antithesis-period-split", "high", ), + ( + # Restated subject across a period: "The goal is not a prettier policy. + # The goal is ..." / 'the transformation is not "X was added." The + # transformation is ...'. The rejected part may be a quotation that + # contains its own period. Requires the SAME noun after the/this/that + # in both sentences (backreference), which keeps ordinary follow-ups + # ("The key is not rotated. The vault is sealed.") out. + re.compile( + rf"(?i)\b(?:the|this|that)\s+(\w+)\s+(?:is|are|was|were)" + rf"(?:\s+not|n{_AP}?t)\s+" + rf"(?:\"[^\"\n]{{1,80}}\"|“[^”\n]{{1,80}}”|[^.;:\n\"“]{{1,80}})" + rf"\.?[\"”]?\s+(?:the|this|that)\s+\1\s+(?:is|are|was|were)\b" + ), + "antithesis-restated-subject", + "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 f7f7d87b..7fcd3871 100644 --- a/tests/test_check_paradigm_shift.py +++ b/tests/test_check_paradigm_shift.py @@ -99,7 +99,53 @@ def test_subject_isnt_just_about_x_its_y(self): self.assertIn("antithesis-subj-not-itis", labels_for(text)) +class TestRestatedSubject(unittest.TestCase): + """'The X is not A. The X is B.' - the subject is repeated instead of 'It is'. + Missed by every other pattern until 2026-10-23 (found in a full-sentence review).""" + + def test_quoted_rejected_part(self): + # telemetry-turns-predatory L67 (2026-10-23): the quote has its own period + text = ( + 'From a threat-modeling perspective, the transformation is not ' + '"surveillance was added." The transformation is that the response ' + 'stage was rewired.' + ) + self.assertIn("antithesis-restated-subject", labels_for(text)) + + def test_unquoted(self): + # iam-blast-radius-architecture-problem L109 + text = "The goal is not a prettier IAM policy. The goal is a smaller blast radius." + self.assertIn("antithesis-restated-subject", labels_for(text)) + + def test_adjective_and_case(self): + # telemetry-turns-predatory L57, with odd spacing and capitals + text = "THE difference is not architectural. The Difference is who acts." + self.assertIn("antithesis-restated-subject", labels_for(text)) + + def test_curly_quotes(self): + text = "The fix was not “just a patch.” The fix was a redesign." + self.assertIn("antithesis-restated-subject", labels_for(text)) + + def test_negative_different_subject(self): + text = "The key is not rotated. The vault is sealed." + self.assertNotIn("antithesis-restated-subject", labels_for(text)) + + def test_negative_no_negation(self): + text = "The goal is a smaller policy. The goal is also cheaper to review." + self.assertNotIn("antithesis-restated-subject", 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 + # Ralph review dropped this label (fixed in rivassec/research-feed). + text = ( + "The problem is not that engineers cannot read JSON. It is that " + "reviewing individual permissions is not the same as reasoning " + "about their consequences." + ) + self.assertIn("antithesis-period-split", labels_for(text)) + def test_classic_form_a(self): text = "It is not a bug, it is a feature." self.assertIn("antithesis-it-not-it-is", labels_for(text))