|
| 1 | +"""Prove the claim checker reads notes, reads them with the right vocabulary, and |
| 2 | +that the vocabulary is the same one every core enforces. |
| 3 | +
|
| 4 | +Extending the check to `notes` is only worth having if three things hold at once, and |
| 5 | +each fails silently on its own: |
| 6 | +
|
| 7 | +* notes are actually matched, or the field is nominally covered and never read; |
| 8 | +* the completeness words do not apply to notes, or the check fires on every artifact |
| 9 | + that states what it was tested against, which is the wording this project asks for; |
| 10 | +* a denial is not treated as a claim, or the same wording is taxed and the allowlist |
| 11 | + grows every time somebody writes a limitation down correctly. |
| 12 | +
|
| 13 | +The vocabulary is pinned to a literal here. The five cores each carry their own copy of |
| 14 | +the checker, and they had already drifted: four spelled the habit stem open, so |
| 15 | +"habitat" matched, and only one had it closed. A checker that quietly enforces a |
| 16 | +different standard per repo is worse than a strict one, and nothing else compares them. |
| 17 | +This file is identical in all five, so a change made in one repo alone fails that |
| 18 | +repo's own CI. |
| 19 | +
|
| 20 | +The expected values are spelled out rather than derived from the patterns under test. |
| 21 | +A fixture built from the constant it verifies moves with the bug. |
| 22 | +""" |
| 23 | +import importlib.util |
| 24 | +import pathlib |
| 25 | +import sys |
| 26 | +import unittest |
| 27 | + |
| 28 | +REPO_ROOT = pathlib.Path(__file__).resolve().parents[3] |
| 29 | +_MODULE_PATH = REPO_ROOT / 'admin' / 'scripts' / 'check_claim_language.py' |
| 30 | + |
| 31 | +# admin/scripts is not a package, so load the module from its path. |
| 32 | +_spec = importlib.util.spec_from_file_location('check_claim_language', _MODULE_PATH) |
| 33 | +ccl = importlib.util.module_from_spec(_spec) |
| 34 | +sys.modules['check_claim_language'] = ccl |
| 35 | +_spec.loader.exec_module(ccl) |
| 36 | + |
| 37 | +# The vocabulary every core is expected to enforce, written out rather than imported. |
| 38 | +EXPECTED_CLAIM = ( |
| 39 | + r'\ball\b|\bevery\b|\bcomplete|\bfull list\b|\bentire\b|' |
| 40 | + r'\bthe user (?:searched|typed|viewed|visited|opened|selected|deleted|' |
| 41 | + r'read|sent|created|hid|chose)\b|\buser[- ](?:created|entered|typed|' |
| 42 | + r'searched|selected|initiated)\b|\bsearched by\b|\btyped by\b|' |
| 43 | + r'\bviewed by\b|\bread by\b|\bmanually\b|\bproves?\b|\bdefinitively\b|' |
| 44 | + r'\balways\b|\breliable|\bvisited\b|\bhabits?\b' |
| 45 | +) |
| 46 | +EXPECTED_NOTES = ( |
| 47 | + r'\bthe user (?:searched|typed|viewed|visited|opened|selected|deleted|' |
| 48 | + r'read|sent|created|hid|chose)\b|\buser[- ](?:created|entered|typed|' |
| 49 | + r'searched|selected|initiated)\b|\bsearched by\b|\btyped by\b|' |
| 50 | + r'\bviewed by\b|\bmanually\b|\bproves?\b|\bdefinitively\b|\balways\b|' |
| 51 | + r'\breliable|\bvisited\b|\bhabits?\b' |
| 52 | +) |
| 53 | +EXPECTED_NEGATION = ( |
| 54 | + r'\b(not|no|never|nor|neither|without|cannot|rather than|instead of|' |
| 55 | + r"isn't|doesn't|don't|does not|do not)\b" |
| 56 | +) |
| 57 | +EXPECTED_FIELDS = ('description', 'name', 'notes') |
| 58 | + |
| 59 | + |
| 60 | +def fires(field, text): |
| 61 | + """True when the vocabulary for `field` reports a match in `text`, negation applied.""" |
| 62 | + pattern = ccl.CHECKED_FIELDS[field] |
| 63 | + hits = list(pattern.finditer(text)) |
| 64 | + if field == 'notes': |
| 65 | + hits = [hit for hit in hits if not ccl.negated(text, hit.start())] |
| 66 | + return bool(hits) |
| 67 | + |
| 68 | + |
| 69 | +class VocabularyParity(unittest.TestCase): |
| 70 | + """The five cores must enforce one standard. Drift in any of them fails here.""" |
| 71 | + |
| 72 | + def test_claim_vocabulary_is_the_shared_one(self): |
| 73 | + self.assertEqual(ccl.CLAIM_PATTERN.pattern, EXPECTED_CLAIM) |
| 74 | + |
| 75 | + def test_notes_vocabulary_is_the_shared_one(self): |
| 76 | + self.assertEqual(ccl.NOTES_PATTERN.pattern, EXPECTED_NOTES) |
| 77 | + |
| 78 | + def test_negation_vocabulary_is_the_shared_one(self): |
| 79 | + self.assertEqual(ccl.NEGATION_PATTERN.pattern, EXPECTED_NEGATION) |
| 80 | + self.assertEqual(ccl.NEGATION_WINDOW, 60) |
| 81 | + |
| 82 | + def test_the_same_three_fields_are_checked(self): |
| 83 | + self.assertEqual(tuple(sorted(ccl.CHECKED_FIELDS)), EXPECTED_FIELDS) |
| 84 | + |
| 85 | + def test_the_habit_stem_is_closed(self): |
| 86 | + # The open stem also matches "habitat". Four cores carried that until 2026-08-29. |
| 87 | + self.assertFalse(ccl.CLAIM_PATTERN.search('habitat')) |
| 88 | + self.assertFalse(ccl.NOTES_PATTERN.search('habitat')) |
| 89 | + self.assertTrue(ccl.CLAIM_PATTERN.search('habits')) |
| 90 | + |
| 91 | + |
| 92 | +class FieldCoverage(unittest.TestCase): |
| 93 | + def test_notes_is_checked(self): |
| 94 | + self.assertIn('notes', ccl.CHECKED_FIELDS) |
| 95 | + |
| 96 | + def test_name_and_description_keep_the_full_vocabulary(self): |
| 97 | + self.assertIs(ccl.CHECKED_FIELDS['name'], ccl.CLAIM_PATTERN) |
| 98 | + self.assertIs(ccl.CHECKED_FIELDS['description'], ccl.CLAIM_PATTERN) |
| 99 | + |
| 100 | + def test_notes_uses_its_own_vocabulary(self): |
| 101 | + self.assertIs(ccl.CHECKED_FIELDS['notes'], ccl.NOTES_PATTERN) |
| 102 | + |
| 103 | + |
| 104 | +class NotesVocabulary(unittest.TestCase): |
| 105 | + # Attribution and certainty mean the same thing in a note as in a description. |
| 106 | + CLAIMS = ( |
| 107 | + 'the term the user typed into the search box', |
| 108 | + 'a page the user visited', |
| 109 | + 'a term the user entered', |
| 110 | + 'this proves the account holder sent it', |
| 111 | + 'the column is always populated', |
| 112 | + 'a reliable record of the conversation', |
| 113 | + ) |
| 114 | + # Notes state what was tested. These must stay silent or the check punishes the |
| 115 | + # coverage discipline it exists alongside. |
| 116 | + COVERAGE = ( |
| 117 | + 'empty on all 18 copies tested', |
| 118 | + 'NULL for every account tested', |
| 119 | + 'the complete table list is given above', |
| 120 | + 'present on the entire corpus', |
| 121 | + 'columns are read by position over a select star', |
| 122 | + 'the sidecar is not read by this artifact', |
| 123 | + ) |
| 124 | + |
| 125 | + def test_claims_fire(self): |
| 126 | + for text in self.CLAIMS: |
| 127 | + with self.subTest(text=text): |
| 128 | + self.assertTrue(fires('notes', text)) |
| 129 | + |
| 130 | + def test_coverage_wording_stays_silent(self): |
| 131 | + for text in self.COVERAGE: |
| 132 | + with self.subTest(text=text): |
| 133 | + self.assertFalse(fires('notes', text)) |
| 134 | + |
| 135 | + def test_notes_drops_exactly_the_two_documented_classes(self): |
| 136 | + """The two vocabularies differ only where the module says they do.""" |
| 137 | + removed = ('all', 'every', 'complete', 'entire', 'full list', 'read by') |
| 138 | + for word in removed: |
| 139 | + with self.subTest(word=word, vocabulary='description'): |
| 140 | + self.assertTrue(ccl.CLAIM_PATTERN.search(word)) |
| 141 | + with self.subTest(word=word, vocabulary='notes'): |
| 142 | + self.assertFalse(ccl.NOTES_PATTERN.search(word)) |
| 143 | + kept = ('the user typed', 'typed by', 'manually', 'proves', 'always', |
| 144 | + 'reliable', 'visited', 'habits', 'user-created') |
| 145 | + for word in kept: |
| 146 | + with self.subTest(word=word): |
| 147 | + self.assertTrue(ccl.CLAIM_PATTERN.search(word)) |
| 148 | + self.assertTrue(ccl.NOTES_PATTERN.search(word)) |
| 149 | + |
| 150 | + def test_description_still_flags_completeness(self): |
| 151 | + # The narrowing applies to notes only. Regression guard on the original behaviour. |
| 152 | + self.assertTrue(fires('description', 'every message in the conversation')) |
| 153 | + self.assertTrue(fires('description', 'all sites the user visited')) |
| 154 | + |
| 155 | + |
| 156 | +class Negation(unittest.TestCase): |
| 157 | + def test_denial_in_the_same_clause_is_not_a_claim(self): |
| 158 | + for text in ( |
| 159 | + 'this store holds suggestions the app downloaded, not terms the user searched for', |
| 160 | + 'their presence does not establish that the user viewed them', |
| 161 | + 'they evidence the app running rather than anything the user chose', |
| 162 | + 'the app stores no user created images', |
| 163 | + ): |
| 164 | + with self.subTest(text=text): |
| 165 | + self.assertFalse(fires('notes', text)) |
| 166 | + |
| 167 | + def test_a_negation_in_the_previous_sentence_does_not_carry(self): |
| 168 | + # "not" governs its own clause. A full stop ends it, so the claim after it stands. |
| 169 | + text = 'The table is not a cache. It records the term the user typed.' |
| 170 | + self.assertTrue(fires('notes', text)) |
| 171 | + |
| 172 | + def test_a_distant_negation_does_not_carry(self): |
| 173 | + # Sixty characters is the window; padding past it must leave the claim visible. |
| 174 | + text = 'not' + ' x' * 45 + ' the term the user typed' |
| 175 | + self.assertTrue(fires('notes', text)) |
| 176 | + |
| 177 | + |
| 178 | +if __name__ == '__main__': |
| 179 | + unittest.main() |
0 commit comments