diff --git a/tests/test_navigation_quotes.py b/tests/test_navigation_quotes.py index 03f6fd8f4..f3b445da2 100644 --- a/tests/test_navigation_quotes.py +++ b/tests/test_navigation_quotes.py @@ -76,6 +76,65 @@ def test_labelled_summary_with_foreign_verdict_word_is_rejected(self): report = self._report('| #1 | [r](../../reviews/r.md) — summary: continuum HOLD; finite ACCEPT |') self.assertEqual([f[0] for f in report['failures']], ['verdict-word-not-in-source']) + def test_prefixed_source_verdict_does_not_authorize_summary(self): + for word in check_doc_quotes.VERDICT: + for prefix in ('UN', 'RE', '_', '9', 'é'): + with self.subTest(word=word, prefix=prefix): + (self.root / 'reviews' / 'r.md').write_text(prefix + word, encoding='utf-8') + report = self._report('| #1 | [r](../../reviews/r.md) — summary: ' + word + ' |') + self.assertEqual((report['checked'], report['summaries'], report['unchecked_offline']), (1, 1, 0)) + self.assertEqual([f[0] for f in report['failures']], ['verdict-word-not-in-source']) + self.assertTrue(report['failures'][0][2].startswith(word + ': ')) + + def test_word_start_and_existing_suffix_families_remain_accepted(self): + for word in check_doc_quotes.VERDICT: + for text in (word, '(' + word + ')', '**' + word + '_SCOPED**'): + with self.subTest(word=word, text=text): + (self.root / 'reviews' / 'r.md').write_text(text, encoding='utf-8') + report = self._report('| #1 | [r](../../reviews/r.md) — summary: ' + word + '_SCOPED |') + self.assertEqual(report['failures'], []) + self.assertEqual(report['checked'], 1) + + def test_prefixed_summary_is_not_an_unprefixed_verdict_claim(self): + (self.root / 'reviews' / 'r.md').write_text('No tracked verdict.', encoding='utf-8') + for word in check_doc_quotes.VERDICT: + with self.subTest(word=word): + report = self._report('| #1 | [r](../../reviews/r.md) — summary: UN' + word + ' |') + self.assertEqual(report['failures'], []) + self.assertEqual(report['summaries'], 1) + + def test_standalone_verdict_after_prefixed_word_is_found(self): + for word in check_doc_quotes.VERDICT: + with self.subTest(word=word): + (self.root / 'reviews' / 'r.md').write_text('UN' + word + '; ' + word, encoding='utf-8') + report = self._report('| #1 | [r](../../reviews/r.md) — summary: ' + word + ' |') + self.assertEqual(report['failures'], []) + + def test_cli_preserves_positive_and_rejects_prefixed_source(self): + import json + import subprocess + + doc = self.root / 'docs' / 'nested' / 'a.md' + flags = ['-B'] + (['-O'] if sys.flags.optimize else []) + ['-S'] + for word in check_doc_quotes.VERDICT: + for prefix, expected in (('', 0), ('UN', 1)): + with self.subTest(word=word, prefix=prefix): + (self.root / 'reviews' / 'r.md').write_text(prefix + word, encoding='utf-8') + doc.write_text(HEADER + '| #1 | [r](../../reviews/r.md) — summary: ' + word + ' |\n', encoding='utf-8') + result = subprocess.run( + [sys.executable, *flags, str(ROOT / 'tools' / 'check_doc_quotes.py'), + str(doc), '--root', self.tmp, '--require-online'], + capture_output=True, check=False, timeout=15) + self.assertEqual(result.returncode, expected) + self.assertEqual(result.stderr, b'') + report = json.loads(result.stdout) + self.assertIs(report['passed'], expected == 0) + self.assertTrue(report['require_online']) + actual = report['documents'][str(doc)] + self.assertEqual((actual['checked'], actual['summaries'], actual['unchecked_offline']), (1, 1, 0)) + self.assertEqual([f[0] for f in actual['failures']], + ['verdict-word-not-in-source'] if expected else []) + def test_passage_without_preceding_link_is_rejected(self): report = self._report('| #1 | “Verdict: ACCEPT” — [r](../../reviews/r.md) |') self.assertEqual([f[0] for f in report['failures']], ['no-source-link']) diff --git a/tools/check_doc_quotes.py b/tools/check_doc_quotes.py index 635505b48..154f28203 100644 --- a/tools/check_doc_quotes.py +++ b/tools/check_doc_quotes.py @@ -14,7 +14,9 @@ ``--github`` (token read from ``GITHUB_TOKEN``); without it the passage is reported as ``unchecked_offline`` and does not fail the check; * a ``summary:`` passage must not use a verdict word (ACCEPT, AMEND, HOLD, CONFIRMED, - VERIFIED, REJECT) that the linked record does not use, under the same offline rule; + VERIFIED, REJECT) that the linked record does not use, under the same offline rule. + The same leading word boundary applies to both texts; suffix forms remain allowed. + This lexical check does not establish semantic agreement or resolve negation; * a passage with no preceding link in its segment fails (``no-source-link``). Header and separator rows are skipped. Exit status 0 only when no checked passage fails; @@ -177,7 +179,8 @@ def check_document(doc_path, root, github=False, token=None): report['failures'].append(['not-verbatim', source, prefix]) else: for word in VERDICT: - if re.search(r'\b' + word, passage) and word not in body: + pattern = r'\b' + word + if re.search(pattern, passage) and not re.search(pattern, body): report['failures'].append(['verdict-word-not-in-source', source, word + ': ' + prefix]) return report