From 29ca89339b7cb347695f668ea6b5a229e605070b Mon Sep 17 00:00:00 2001 From: Dylan Roy <249851642+d6g8k5htny-coder@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:35:02 -0700 Subject: [PATCH] Bind retrieved navigation sources to complete GitHub citation identities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dylan Roy — delegated AI engineering. Actual author OpenAI / GPT-6 Astra Pro, navigation-citation-identity-audit-20261006, pickup336/6009316387 and scope336/6009380396. Isolated child of reviewed #344; no mutation of #336/#344 or other active integration branches. Fix NAV-SOURCE-IDENTITY-01: require full citation match plus returned integer ID, browser locator and correct parent issue/PR URL before using body text. Repository-wide comment endpoints do not themselves authenticate the PR in a citation. Preserve existing lexical, quotation, offline, token, and error contracts. Add16 synthetic-HTTP methods and wire them into existing navigation workflow. Exact predecessor: final suite60 intended assertion failures and2 malformed-body errors per mode; repaired16/16 pass in both CPython3.13.5 modes. No live network/full-repository/Lean execution claimed locally. No current document mismatch alleged. Scientific effect NONE; organizational-independence credit0; separate source review/current-candidate checks and eligible integration required. --- .github/workflows/navigation-links.yml | 5 +- tests/test_navigation_source_identity.py | 226 +++++++++++++++++++++++ tools/check_doc_quotes.py | 42 ++++- 3 files changed, 268 insertions(+), 5 deletions(-) create mode 100644 tests/test_navigation_source_identity.py diff --git a/.github/workflows/navigation-links.yml b/.github/workflows/navigation-links.yml index aaa99e776..619464bfd 100644 --- a/.github/workflows/navigation-links.yml +++ b/.github/workflows/navigation-links.yml @@ -9,6 +9,7 @@ name: Navigation document links and quotations - 'tools/check_doc_quotes.py' - 'tests/test_navigation_links.py' - 'tests/test_navigation_quotes.py' + - 'tests/test_navigation_source_identity.py' - '.github/workflows/navigation-links.yml' workflow_dispatch: @@ -36,6 +37,6 @@ jobs: python -B -O -S tools/check_doc_links.py PROOF_INDEX.md docs/integration/*.md python -B -S tools/check_doc_quotes.py --github --require-online PROOF_INDEX.md docs/integration/2026-09-29-navigation-refresh.md python -B -O -S tools/check_doc_quotes.py --github --require-online PROOF_INDEX.md docs/integration/2026-09-29-navigation-refresh.md - python -B -S -m unittest -v tests.test_navigation_links tests.test_navigation_quotes - python -B -O -S -m unittest -v tests.test_navigation_links tests.test_navigation_quotes + python -B -S -m unittest -v tests.test_navigation_links tests.test_navigation_quotes tests.test_navigation_source_identity + python -B -O -S -m unittest -v tests.test_navigation_links tests.test_navigation_quotes tests.test_navigation_source_identity git diff --exit-code diff --git a/tests/test_navigation_source_identity.py b/tests/test_navigation_source_identity.py new file mode 100644 index 000000000..435bfdde2 --- /dev/null +++ b/tests/test_navigation_source_identity.py @@ -0,0 +1,226 @@ +"""Source identity contracts; HTTP responses are mocked, not live fetch evidence.""" +import contextlib +import importlib.util +import io +import json +from pathlib import Path +import sys +import tempfile +import unittest +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] +SOURCE = ROOT / 'tools/check_doc_quotes.py' +spec = importlib.util.spec_from_file_location('quote_identity_subject', SOURCE) +quotes = importlib.util.module_from_spec(spec) +spec.loader.exec_module(quotes) + +KINDS = ('issuecomment', 'pullrequestreview', 'discussion_r') +OWNER, REPO, PR, ID = 'example', 'research', '71', 7001 +API = 'https://api.github.com/repos/' + OWNER + '/' + REPO +BODY = 'Evidence text. ACCEPT_SCOPED at fixed dimension; not global.' + + +def target(kind, pr=PR, ident=ID): + return f'https://github.com/{OWNER}/{REPO}/pull/{pr}#{kind}-{ident}' + + +def payload(kind): + result = {'id': ID, 'body': BODY, 'html_url': target(kind)} + parent_key = 'issue_url' if kind == 'issuecomment' else 'pull_request_url' + parent_type = 'issues' if kind == 'issuecomment' else 'pulls' + result[parent_key] = f'{API}/{parent_type}/{PR}' + # Review objects have no top-level REST `url` field; do not require one. + if kind == 'issuecomment': + result['url'] = f'{API}/issues/comments/{ID}' + elif kind == 'discussion_r': + result['url'] = f'{API}/pulls/comments/{ID}' + result['html_url'] = target(kind).replace('discussion_r-', 'discussion_r') + return result + + +class CitationIdentity(unittest.TestCase): + def invoke(self, url, record, passage=None, extra=None): + if passage is None: + passage = '“Evidence text.”' + with tempfile.TemporaryDirectory(prefix='quote-identity-') as td: + doc = Path(td) / 'navigation.md' + doc.write_text('| source | statement |\n|---|---|\n' + f'| [record]({url}) | [record]({url}) {passage} |\n', encoding='utf-8') + output = io.StringIO() + def response(request, timeout): + return io.BytesIO(json.dumps(record).encode()) + with patch.object(quotes.urllib.request, 'urlopen', side_effect=response) as fetch, \ + contextlib.redirect_stdout(output): + rc = quotes.main([str(doc), '--root', td, '--github', '--require-online', *(extra or [])]) + report = json.loads(output.getvalue()) + return rc, report, report['documents'][str(doc)], fetch + + def test_all_three_genuine_record_shapes(self): + for kind in KINDS: + with self.subTest(kind=kind): + rc, report, doc, fetch = self.invoke(target(kind), payload(kind)) + self.assertEqual(rc, 0) + self.assertTrue(report['passed']) + self.assertEqual((doc['checked'], doc['unchecked_offline']), (1, 0)) + self.assertEqual(doc['failures'], []) + self.assertEqual(fetch.call_count, 1) + req = fetch.call_args.args[0] + suffix = f'/issues/comments/{ID}' if kind == 'issuecomment' else \ + f'/pulls/{PR}/reviews/{ID}' if kind == 'pullrequestreview' else f'/pulls/comments/{ID}' + self.assertEqual(req.full_url, API + suffix) + + def test_other_parent_is_rejected_even_when_body_matches(self): + for kind in KINDS: + with self.subTest(kind=kind): + record = payload(kind) + key = 'issue_url' if kind == 'issuecomment' else 'pull_request_url' + record[key] = record[key].rsplit('/', 1)[0] + '/999' + rc, report, doc, _ = self.invoke(target(kind), record) + self.assertEqual(rc, 1) + self.assertFalse(report['passed']) + self.assertEqual([f[0] for f in doc['failures']], ['source-identity-mismatch']) + self.assertEqual(doc['unchecked_offline'], 0) + + def test_mislinked_pr_cannot_borrow_repository_wide_comment(self): + for kind in ('issuecomment', 'discussion_r'): + with self.subTest(kind=kind): + rc, _, doc, fetch = self.invoke(target(kind, pr='999'), payload(kind)) + self.assertEqual(fetch.call_count, 1) + self.assertEqual(rc, 1) + self.assertEqual([f[0] for f in doc['failures']], ['source-identity-mismatch']) + + def test_wrong_id_or_id_type(self): + for kind in KINDS: + for bad in (ID + 1, str(ID), True, None): + with self.subTest(kind=kind, bad=bad): + record = payload(kind) + record['id'] = bad + rc, _, doc, _ = self.invoke(target(kind), record) + self.assertEqual(rc, 1) + self.assertEqual([f[0] for f in doc['failures']], ['source-identity-mismatch']) + + def test_html_locator_must_bind_repository_pr_kind_and_id(self): + for kind in KINDS: + original = payload(kind)['html_url'] + for bad in (original.replace('/research/', '/other/'), + original.replace('/pull/71', '/pull/999'), + original.replace(str(ID), str(ID + 1)), + original + '-extra', + original.replace('https://github.com/', 'https://example.invalid/')): + with self.subTest(kind=kind, bad=bad): + record = payload(kind) + record['html_url'] = bad + rc, _, doc, _ = self.invoke(target(kind), record) + self.assertEqual(rc, 1) + self.assertEqual([f[0] for f in doc['failures']], ['source-identity-mismatch']) + + def test_missing_identity_fields(self): + for kind in KINDS: + fields = ['id', 'html_url', 'issue_url' if kind == 'issuecomment' else 'pull_request_url'] + for field in fields: + with self.subTest(kind=kind, missing=field): + record = payload(kind) + del record[field] + rc, _, doc, _ = self.invoke(target(kind), record) + self.assertEqual(rc, 1) + self.assertEqual([f[0] for f in doc['failures']], ['source-identity-mismatch']) + + def test_url_suffix_does_not_get_truncated_into_a_valid_citation(self): + for kind in KINDS: + for suffix in ('garbage', '/extra', '?extra=1', '#second', '\n'): + with self.subTest(kind=kind, suffix=repr(suffix)): + # A raw newline is not a table cell URL; exercise Sources directly instead. + if suffix == '\n': + with patch.object(quotes.urllib.request, 'urlopen', return_value=io.BytesIO(json.dumps(payload(kind)).encode())): + _, status = quotes.Sources('/', '/', True, None).body(target(kind) + suffix) + self.assertNotEqual(status, 'ok') + continue + rc, report, doc, fetch = self.invoke(target(kind) + suffix, payload(kind)) + self.assertEqual(rc, 1) + self.assertFalse(report['passed']) + self.assertEqual(fetch.call_count, 0) + self.assertGreater(len(doc['failures']), 0) + + def test_identity_check_applies_to_summaries_as_well_as_quotes(self): + record = payload('issuecomment') + record['issue_url'] = f'{API}/issues/999' + rc, _, doc, _ = self.invoke(target('issuecomment'), record, passage='summary: ACCEPT_SCOPED') + self.assertEqual(rc, 1) + self.assertEqual([f[0] for f in doc['failures']], ['source-identity-mismatch']) + + def test_valid_but_false_quote_still_rejected(self): + rc, _, doc, _ = self.invoke(target('issuecomment'), payload('issuecomment'), passage='“Different claim.”') + self.assertEqual(rc, 1) + self.assertEqual([f[0] for f in doc['failures']], ['not-verbatim']) + + def test_case_insensitive_repo_identity_and_canonical_anchor(self): + for kind in KINDS: + record = payload(kind) + for key, value in list(record.items()): + if isinstance(value, str) and 'example/research' in value: + record[key] = value.replace('example/research', 'Example/Research') + rc, _, _, _ = self.invoke(target(kind), record) + self.assertEqual(rc, 0) + + def test_empty_review_body_retains_legacy_summary_scope(self): + record = payload('pullrequestreview') + record['body'] = None + rc, _, _, _ = self.invoke(target('pullrequestreview'), record, passage='summary: no tracked vocabulary') + self.assertEqual(rc, 0) # Explicitly NOT semantic-entailment validation. + + def test_offline_path_performs_no_http(self): + with patch.object(quotes.urllib.request, 'urlopen') as fetch: + self.assertEqual(quotes.Sources('/', '/', False, None).body(target('issuecomment')), (None, 'offline')) + fetch.assert_not_called() + + + def test_lexical_and_quotation_policies_remain_separate(self): + record = payload('issuecomment') + record['body'] = 'UNVERIFIED' + rc, _, doc, _ = self.invoke(target('issuecomment'), record, passage='summary: VERIFIED') + self.assertEqual(rc, 1) + self.assertEqual([f[0] for f in doc['failures']], ['verdict-word-not-in-source']) + rc, _, _, _ = self.invoke(target('issuecomment'), record, passage='“VERIFIED”') + self.assertEqual(rc, 0) # Retained substring excerpt policy, not whole-word quotation. + record['body'] = 'not VERIFIED' + rc, _, _, _ = self.invoke(target('issuecomment'), record, passage='summary: VERIFIED') + self.assertEqual(rc, 0) # Retained absence of negation/entailment analysis. + + def test_missing_or_nonstr_body_is_not_a_valid_source(self): + for value in ([], 7, True): + with self.subTest(body=value): + record = payload('issuecomment') + record['body'] = value + rc, _, doc, _ = self.invoke(target('issuecomment'), record) + self.assertEqual(rc, 1) + self.assertEqual([f[0] for f in doc['failures']], ['source-identity-mismatch']) + record = payload('issuecomment') + del record['body'] + rc, _, doc, _ = self.invoke(target('issuecomment'), record, passage='summary: no tracked vocabulary') + self.assertEqual(rc, 1) + self.assertEqual([f[0] for f in doc['failures']], ['source-identity-mismatch']) + + def test_failed_transport_and_malformed_json_keep_failure_class(self): + from urllib.error import HTTPError, URLError + for error in (HTTPError('https://api.github.com/', c, 'failure', {}, None) for c in (401, 403, 404, 429, 500, 503)): + with self.subTest(code=error.code), patch.object(quotes.urllib.request, 'urlopen', side_effect=error): + self.assertEqual(quotes.Sources('/', '/', True, None).body(target('issuecomment')), (None, 'unreachable')) + for error in (TimeoutError('timeout'), URLError('DNS fixture')): + with self.subTest(error=type(error).__name__), patch.object(quotes.urllib.request, 'urlopen', side_effect=error): + self.assertEqual(quotes.Sources('/', '/', True, None).body(target('issuecomment')), (None, 'unreachable')) + with patch.object(quotes.urllib.request, 'urlopen', return_value=io.BytesIO(b'{invalid')): + self.assertEqual(quotes.Sources('/', '/', True, None).body(target('issuecomment')), (None, 'unreachable')) + + def test_cached_rejection_cannot_become_success(self): + record = payload('issuecomment') + record['issue_url'] = f'{API}/issues/999' + source = quotes.Sources('/', '/', True, None) + with patch.object(quotes.urllib.request, 'urlopen', return_value=io.BytesIO(json.dumps(record).encode())) as fetch: + self.assertEqual(source.body(target('issuecomment')), (None, 'identity-mismatch')) + self.assertEqual(source.body(target('issuecomment')), (None, 'identity-mismatch')) + self.assertEqual(fetch.call_count, 1) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/tools/check_doc_quotes.py b/tools/check_doc_quotes.py index 154f28203..e62bf5a32 100644 --- a/tools/check_doc_quotes.py +++ b/tools/check_doc_quotes.py @@ -12,7 +12,9 @@ Markdown emphasis, curly quotes/dashes and table-cell pipe escapes; * a GitHub pull-request review / comment link is checked against the live body only with ``--github`` (token read from ``GITHUB_TOKEN``); without it the passage is reported as - ``unchecked_offline`` and does not fail the check; + ``unchecked_offline`` and does not fail the check. Online records must match the + complete citation's record ID, repository, PR, kind and returned parent URL; + partial URLs and mismatched records never supply a checked passage; * 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. The same leading word boundary applies to both texts; suffix forms remain allowed. @@ -84,16 +86,48 @@ def table_data_rows(lines): yield index + 1, line +def record_matches(match, data): + """Bind a returned GitHub record to its full citation, not just its body. + + Comments are fetched through repository-wide endpoints: their IDs alone do + not verify the PR named in the browser URL. Use both returned locators and + the integer record ID. Review objects need not have a top-level REST `url`. + This checks identity consistency, not semantic entailment or review quality. + """ + if not isinstance(data, dict) or type(data.get('id')) is not int: + return False + owner, repo, number, kind, ident = match.groups() + if data['id'] != int(ident): + return False + if 'body' not in data or (data['body'] is not None and not isinstance(data['body'], str)): + return False + html = data.get('html_url') + actual = GITHUB.fullmatch(html) if isinstance(html, str) else None + if actual is None: + return False + actual_owner, actual_repo, actual_number, actual_kind, actual_id = actual.groups() + if (actual_owner.casefold(), actual_repo.casefold(), int(actual_number), actual_kind, int(actual_id)) != ( + owner.casefold(), repo.casefold(), int(number), kind, int(ident)): + return False + key = 'issue_url' if kind == 'issuecomment' else 'pull_request_url' + endpoint = 'issues' if kind == 'issuecomment' else 'pulls' + parent = data.get(key) + if not isinstance(parent, str): + return False + expected = 'https://api.github.com/repos/%s/%s/%s/%s' % (owner, repo, endpoint, number) + return parent.casefold() == expected.casefold() + + class Sources: def __init__(self, root, doc_dir, github, token): self.root, self.doc_dir, self.github, self.token = root, doc_dir, github, token self.cache = {} def body(self, target): - """Return (normalised body, status) with status in {'ok','offline','unreachable','missing'}.""" + """Return (normalised body, status) with status in {'ok','offline','unreachable','missing','identity-mismatch'}.""" if target in self.cache: return self.cache[target] - match = GITHUB.match(target) + match = GITHUB.fullmatch(target) if match: if not self.github: result = (None, 'offline') @@ -130,6 +164,8 @@ def _fetch(self, match): data = json.load(response) except Exception: # noqa: BLE001 - any transport failure is reported, never hidden return (None, 'unreachable') + if not record_matches(match, data): + return (None, 'identity-mismatch') return (normalise(data.get('body') or ''), 'ok')