diff --git a/admin/scripts/check_claim_language.py b/admin/scripts/check_claim_language.py index f3df965..6f6f6a7 100644 --- a/admin/scripts/check_claim_language.py +++ b/admin/scripts/check_claim_language.py @@ -122,24 +122,9 @@ # habitat-related artifact ever lands here, close the stem rather than # allowlisting the artifact. CLAIM_PATTERN = re.compile( - r'\ball\b' - r'|\bevery\b' - r'|\bcomplete' - r'|\bfull list\b' - r'|\bentire\b' - r'|\bthe user (?:searched|typed|viewed|visited|opened|selected|deleted|read|sent' - r'|created|hid|chose)\b' - r'|\buser[- ](?:created|entered|typed|searched|selected|initiated)\b' - r'|\b(?:searched|typed|viewed|read|entered|created|sent|opened|selected|delet' - r'ed|visited|chosen|hidden|initiated) by (?:the |a |an )?(?:user|account holder|device owner|subject|owner)\b' - r'|\bmanually\b' - r'|\bproves?\b' - r'|\bdefinitively\b' - r'|\balways\b' - r'|\breliable' - r'|\bvisited\b' - r'|\bhabits?\b', - re.IGNORECASE) + r"\ball\b|\bevery\b|\bcomplete|\bfull list\b|\bentire\b|\b(?:the|a|an) (?:\w+ )?(?:user|account holder|device owner|subject|owner) (?:searched|typed|viewed|read|entered|created|sent|opened|selected|deleted|visited|chose|chosen|hid|hidden|initiated|follows|saved|added|grouped|submitted|marked|tracked|joined|reacted|installed|removed|configured|voted|opted)\b|\buser[- ](?:created|entered|typed|searched|selected|initiated)\b|\b(?:searched|typed|viewed|read|entered|created|sent|opened|selected|deleted|visited|chosen|hidden|initiated) by (?:the |a |an )?(?:user|account holder|device owner|subject|owner)\b|\bmanually\b|\bproves?\b|\bdefinitively\b|\balways\b|\breliable|\bvisited\b|\bhabits?\b", + re.IGNORECASE, +) # `notes` reaches the examiner too, in the report and in the artifact info modal, so # the same standard applies to it. It cannot use the same vocabulary, because notes do @@ -156,19 +141,9 @@ # What is left is attribution and certainty, which mean the same thing in a note as in # a description. NOTES_PATTERN = re.compile( - r'\bthe user (?:searched|typed|viewed|visited|opened|selected|deleted|read|sent' - r'|created|hid|chose)\b' - r'|\buser[- ](?:created|entered|typed|searched|selected|initiated)\b' - r'|\b(?:searched|typed|viewed|read|entered|created|sent|opened|selected|delet' - r'ed|visited|chosen|hidden|initiated) by (?:the |a |an )?(?:user|account holder|device owner|subject|owner)\b' - r'|\bmanually\b' - r'|\bproves?\b' - r'|\bdefinitively\b' - r'|\balways\b' - r'|\breliable' - r'|\bvisited\b' - r'|\bhabits?\b', - re.IGNORECASE) + r"\b(?:the|a|an) (?:\w+ )?(?:user|account holder|device owner|subject|owner) (?:searched|typed|viewed|read|entered|created|sent|opened|selected|deleted|visited|chose|chosen|hid|hidden|initiated|follows|saved|added|grouped|submitted|marked|tracked|joined|reacted|installed|removed|configured|voted|opted)\b|\buser[- ](?:created|entered|typed|searched|selected|initiated)\b|\b(?:searched|typed|viewed|read|entered|created|sent|opened|selected|deleted|visited|chosen|hidden|initiated) by (?:the |a |an )?(?:user|account holder|device owner|subject|owner)\b|\bmanually\b|\bproves?\b|\bdefinitively\b|\balways\b|\breliable|\bvisited\b|\bhabits?\b", + re.IGNORECASE, +) # A note that *denies* a claim uses the same words as one that makes it: "not terms the # user searched for", "does not establish that the user viewed them". That denial is the diff --git a/admin/test/scripts/test_check_claim_language.py b/admin/test/scripts/test_check_claim_language.py index e3559cd..fcc7ad6 100644 --- a/admin/test/scripts/test_check_claim_language.py +++ b/admin/test/scripts/test_check_claim_language.py @@ -36,23 +36,10 @@ # The vocabulary every core is expected to enforce, written out rather than imported. EXPECTED_CLAIM = ( - r'\ball\b|\bevery\b|\bcomplete|\bfull list\b|\bentire\b|' - r'\bthe user (?:searched|typed|viewed|visited|opened|selected|deleted|' - r'read|sent|created|hid|chose)\b|\buser[- ](?:created|entered|typed|' - r'searched|selected|initiated)\b|\b(?:searched|typed|viewed|read|' - r'entered|created|sent|opened|selected|deleted|visited|chosen|hidden|' - r'initiated) by (?:the |a |an )?(?:user|account holder|device owner|' - r'subject|owner)\b|\bmanually\b|\bproves?\b|\bdefinitively\b|' - r'\balways\b|\breliable|\bvisited\b|\bhabits?\b' + r"\ball\b|\bevery\b|\bcomplete|\bfull list\b|\bentire\b|\b(?:the|a|an) (?:\w+ )?(?:user|account holder|device owner|subject|owner) (?:searched|typed|viewed|read|entered|created|sent|opened|selected|deleted|visited|chose|chosen|hid|hidden|initiated|follows|saved|added|grouped|submitted|marked|tracked|joined|reacted|installed|removed|configured|voted|opted)\b|\buser[- ](?:created|entered|typed|searched|selected|initiated)\b|\b(?:searched|typed|viewed|read|entered|created|sent|opened|selected|deleted|visited|chosen|hidden|initiated) by (?:the |a |an )?(?:user|account holder|device owner|subject|owner)\b|\bmanually\b|\bproves?\b|\bdefinitively\b|\balways\b|\breliable|\bvisited\b|\bhabits?\b" ) EXPECTED_NOTES = ( - r'\bthe user (?:searched|typed|viewed|visited|opened|selected|deleted|' - r'read|sent|created|hid|chose)\b|\buser[- ](?:created|entered|typed|' - r'searched|selected|initiated)\b|\b(?:searched|typed|viewed|read|' - r'entered|created|sent|opened|selected|deleted|visited|chosen|hidden|' - r'initiated) by (?:the |a |an )?(?:user|account holder|device owner|' - r'subject|owner)\b|\bmanually\b|\bproves?\b|\bdefinitively\b|' - r'\balways\b|\breliable|\bvisited\b|\bhabits?\b' + r"\b(?:the|a|an) (?:\w+ )?(?:user|account holder|device owner|subject|owner) (?:searched|typed|viewed|read|entered|created|sent|opened|selected|deleted|visited|chose|chosen|hid|hidden|initiated|follows|saved|added|grouped|submitted|marked|tracked|joined|reacted|installed|removed|configured|voted|opted)\b|\buser[- ](?:created|entered|typed|searched|selected|initiated)\b|\b(?:searched|typed|viewed|read|entered|created|sent|opened|selected|deleted|visited|chosen|hidden|initiated) by (?:the |a |an )?(?:user|account holder|device owner|subject|owner)\b|\bmanually\b|\bproves?\b|\bdefinitively\b|\balways\b|\breliable|\bvisited\b|\bhabits?\b" ) EXPECTED_NEGATION = ( r'\b(not|no|never|nor|neither|without|cannot|rather than|instead of|' @@ -258,6 +245,48 @@ def test_the_two_vocabularies_agree_on_the_passive_family(self): self.assertEqual(bool(ccl.CLAIM_PATTERN.search(probe)), bool(ccl.NOTES_PATTERN.search(probe))) +class ActiveFormsAreAnchoredOnAPerson(unittest.TestCase): + """The active family must fire on a person subject however it is worded, and stay + silent when the subject is not a person. + + Anchored only on the literal "the user", the check was evadable by naming the app in + front of the noun ("the Flipboard user follows", "the Hinge account holder selected") + or by a different determiner ("a user tracked"), with nothing about the attribution + changed. The subject now takes the same determiners and person-noun set as the + passive family, plus an optional descriptor, and the verb set adds the conduct words + that recur in these descriptions. + """ + + PERSON_SUBJECTS = ('the user', 'a user', 'the Flipboard user', + 'the Hinge account holder', 'the device owner', 'an account owner') + CONDUCT = ('follows', 'saved', 'added', 'grouped', 'submitted', 'marked', + 'tracked', 'joined', 'reacted', 'selected', 'typed', 'visited') + + def test_a_person_subject_fires_however_worded(self): + for subject in self.PERSON_SUBJECTS: + for field in ('description', 'notes'): + with self.subTest(subject=subject, field=field): + self.assertTrue(fires(field, f'items {subject} added to the list')) + + def test_each_conduct_verb_fires_after_a_person(self): + for verb in self.CONDUCT: + for field in ('description', 'notes'): + with self.subTest(verb=verb, field=field): + self.assertTrue(fires(field, f'things the user {verb}')) + + def test_a_non_person_subject_in_the_active_voice_stays_silent(self): + for text in ('the app added a row', 'the parser opened the file', + 'the sync service created a record', 'the indexer marked it read', + 'the calling app submitted the query'): + for field in ('description', 'notes'): + with self.subTest(text=text, field=field): + self.assertFalse(fires(field, text)) + + def test_the_motivating_case_is_caught(self): + # ALEAPP #1332 corrected this description; the anchoring is what stops the class. + self.assertTrue(fires('description', + 'Topics and magazines the Flipboard user follows')) + if __name__ == '__main__': unittest.main() diff --git a/scripts/artifacts/instagramNointerest.py b/scripts/artifacts/instagramNointerest.py index 8549921..f00ca55 100755 --- a/scripts/artifacts/instagramNointerest.py +++ b/scripts/artifacts/instagramNointerest.py @@ -1,7 +1,7 @@ __artifacts_v2__ = { "instagramNointerest": { "name": "Instagram Archive - Accounts No Interest", - "description": "Parses accounts the user marked as not interested from an Instagram data archive", + "description": "Parses accounts marked as not interested from an Instagram data archive", "author": "@AlexisBrignoni", "creation_date": "2021-08-27", "last_update_date": "2026-06-27",