From be479fd575307f3f60a5f41fd92af37c2fcf0af0 Mon Sep 17 00:00:00 2001 From: Arham-Tahir64 Date: Thu, 15 Oct 2026 16:09:21 -0600 Subject: [PATCH] Audit PR2493 Discussion pagination and cache failure coverage --- .../DEPENDENCIES.json | 23 ++ .../README.md | 154 +++++++ .../RESULT.json | 56 +++ .../SOURCES.json | 24 ++ .../TASK.json | 134 +++++++ .../VERIFY.json | 30 ++ .../sources/baseline.py | 377 +++++++++++++++++ .../sources/candidate-tests.py | 185 +++++++++ .../sources/candidate.py | 378 ++++++++++++++++++ .../verify.py | 272 +++++++++++++ 10 files changed, 1633 insertions(+) create mode 100644 research/overlap-batching-independent-audit/DEPENDENCIES.json create mode 100644 research/overlap-batching-independent-audit/README.md create mode 100644 research/overlap-batching-independent-audit/RESULT.json create mode 100644 research/overlap-batching-independent-audit/SOURCES.json create mode 100644 research/overlap-batching-independent-audit/TASK.json create mode 100644 research/overlap-batching-independent-audit/VERIFY.json create mode 100644 research/overlap-batching-independent-audit/sources/baseline.py create mode 100644 research/overlap-batching-independent-audit/sources/candidate-tests.py create mode 100644 research/overlap-batching-independent-audit/sources/candidate.py create mode 100644 research/overlap-batching-independent-audit/verify.py diff --git a/research/overlap-batching-independent-audit/DEPENDENCIES.json b/research/overlap-batching-independent-audit/DEPENDENCIES.json new file mode 100644 index 0000000000..1898f20acb --- /dev/null +++ b/research/overlap-batching-independent-audit/DEPENDENCIES.json @@ -0,0 +1,23 @@ +[ + { + "local_path": "sources/baseline.py", + "commit": "6ca6f217708b075814de409ad693fea9a2aaa760", + "path": "scripts/find_overlap.py", + "git_blob": "de0bca1f9e3ee60d0f1659a87af3869bae8c90da", + "sha256": "890902252573117253ecd536ea09a04115092c7255dfe2ebcf96f7e2d337fdbd" + }, + { + "local_path": "sources/candidate.py", + "commit": "a459ee34fad0488691beba8441e0a31e103b91a5", + "path": "scripts/find_overlap.py", + "git_blob": "5bdee10f1a5b324088d16f5a271ed57cec83c6c8", + "sha256": "d17c67167d6654dbea9763cab21bf9b54e929aebfcf18c7ea1b7f170dc6db998" + }, + { + "local_path": "sources/candidate-tests.py", + "commit": "a459ee34fad0488691beba8441e0a31e103b91a5", + "path": "tests/test_research_discussion_batching.py", + "git_blob": "1ad26efd6e72e8d07a60b544f1be8e02e219a5e7", + "sha256": "948415ece070e2e9f48c01499d618f60f29482318621b1ff0981643333312d59" + } +] diff --git a/research/overlap-batching-independent-audit/README.md b/research/overlap-batching-independent-audit/README.md new file mode 100644 index 0000000000..6da2497884 --- /dev/null +++ b/research/overlap-batching-independent-audit/README.md @@ -0,0 +1,154 @@ +# Independent pagination and failure audit of PR2493 + +Arham Garg's [PR2493](https://github.com/CrocSwap/integer-mult-bounds/pull/2493) +batches the initial Discussion/comment/reply pages used by the research overlap +helper. This package independently corroborates its ordered-record and cache +failure behavior at **`a459ee34fad0488691beba8441e0a31e103b91a5`**. It adds no +collector implementation and claims no new measured speedup or multiplication +result. The selected record and production helper are unchanged. + +The added evidence is a boundary cross-product and failure injection at every +request position, rather than a second implementation of the batching change. +The API model and direct record traversal here do not import the predecessor's +test harness. Both harnesses use the usual static-graph/cursor simulation; +this is separate implementation and expanded finite coverage, not an independent +human review or a proof for every possible API response. + +## Exact sources + +[DEPENDENCIES.json](DEPENDENCIES.json) records commit, path, Git blob and SHA256 +for three byte-identical source exports: + +| Input | Commit | Purpose | +| --- | --- | --- | +| `sources/baseline.py` | `6ca6f217708b075814de409ad693fea9a2aaa760` | Current-main unbatched collector and cache implementation | +| `sources/candidate.py` | `a459ee34fad0488691beba8441e0a31e103b91a5` | PR2493's actual collector and cache implementation | +| `sources/candidate-tests.py` | same PR2493 head | Six original tests, replayed unchanged in fresh normal/optimized processes | + +All three inputs are authenticated before any target module executes. Sources +are compiled directly from those bytes, avoiding saved bytecode. The audit +needs neither Git nor network access. [SOURCES.json](SOURCES.json) retains main, +the selected record and the assigned task as historical provenance. There are +no live production-script pins to become stale if the batching change is adopted. +The generic package metadata check uses repository history; the direct checker +works in a Git-free package export. + +## Finite result and failure coverage + +The immutable corpus is generated from the full Cartesian product: + +- Discussion counts: `0, 1, 49, 50, 51, 101`. +- Busy-thread comment counts and selected-comment reply counts, independently: + `0, 1, 4, 5, 6, 99, 100, 101, 104, 105, 106, 205`. +- Both the first and last Discussion are busy; other threads have small pages. + Reply-heavy comments occur at indices 0, 5 and the last comment. This exercises + replies delivered with initial and continuation comment pages. + +The 864 parameter tuples yield **710 distinct graphs** after exact canonical +deduplication. Both collectors match the direct, cursor-free traversal on every +field and record position: **2,062,777 records per collector**. Equal body text +on different nodes prevents text-only comparison from hiding an order/identity +error. Unicode newline characters are retained. Opaque cursors encode their +connection owner and reject reuse in another thread or comment. + +On the separate 51-thread / 205-comment / 205-reply busy fixture, every actual +request position is failed with a GraphQL-style exception, transport exception, +or malformed missing-connection response. Each is tested both with and without +previous complete evidence: + +| Collector | Request positions | Failure cases | +| --- | ---: | ---: | +| Main baseline | 135 | 810 | +| PR2493 | 84 | 504 | + +All **1,314 cases** retain a warning. With old evidence, its complete source, +records and original timestamp survive in memory and on disk. On a first fetch, +no partial Discussion source is admitted. The separately fetched PR source may +still refresh, matching the existing per-source cache contract. + +Three actual collector mutants stop listing, comment or reply pagination early; +the direct traversal rejects all three. Three modified source exports fail +authentication. The candidate's six original tests pass in both normal and +optimized subprocesses. [RESULT.json](RESULT.json) is the deterministic receipt; +normal verification compares it without writing package files. + +For a fixed 101-small-thread corpus, requests fall from **104 to 3** with equal +records. This is a finite request-count corroboration. It is not a new network +timing measurement, nor a claim that busy threads always use fewer requests. +Arham Garg's existing timing evidence retains its own corpus and environment. + +## Reproduce + +Standard-library Python 3.12, from the repository root: + +```sh +python3.12 -I -B scripts/research_package.py --package research/overlap-batching-independent-audit +``` + +The registered commands run the complete audit normally and with `-O`. They +also replay the predecessor suite in both modes. To check the exact pinned +package without Git or the repository: + +```sh +python3.12 -I -B research/overlap-batching-independent-audit/verify.py +python3.12 -I -O -B research/overlap-batching-independent-audit/verify.py +``` + +Expected: `PASS_OVERLAP_BATCHING_INDEPENDENT_AUDIT`, 710 static graphs, and the +counts above. `--write` explicitly regenerates only this package's receipt. +On restricted Windows shells, set `TMP` and `TEMP` to an existing writable +workspace directory before running; the checker uses temporary fixtures. + +Executed on Windows x64, 2026-10-15: the registered CPython **3.12.14** replay +passes both commands. An optimized Git-free export also passes from an unrelated +directory with spaces, leaving all ten package files byte-identical. The initial +direct CPython **3.14.6** audit passes too. These are local assisted checks; no +hosted-CI or independent human result is claimed. TASK.json records the commands, +environment and final freshness check. + +## Limits and integration handoff + +This checks finite static API fixtures and specified failures. It does not prove +atomicity under concurrent board edits, certify GitHub availability/rate limits, +recheck every future source head, or provide a live timing ratio. Authenticated +source exports establish which code ran, not a general trust guarantee for code. + +PR2493's two reported frozen-dependent failures reproduce on this untouched +main base: + +```sh +python3.12 -B scripts/research_package.py --package research/overlap-path-index --check-only +# source blob provenance mismatch: scripts/find_overlap.py +python3.12 -B scripts/research_package.py --package research/research-package-pin-batching --check-only +# working source drift: tests/test_research_onboarding.py +``` + +They remain unresolved. This package does not edit frozen evidence, adopt the +collector, resolve the wider [PR2255 live-pin audit](https://github.com/CrocSwap/integer-mult-bounds/pull/2255), +or claim those dependent replays passed. Supplier/native/hosting/precision and +selected full cold replays are unrelated to this scoped audit and were not run. + +## Open next questions + +Can PR2493 be integrated with an explicit disposition for the two dependent +failures? This matters because passing the collector tests alone does not admit +frozen package changes. Reproduce the two commands above and record a maintainer +source-binding decision. That policy/integration decision is the blocker; more +static pagination examples are not proposed as a prerequisite. The structured +question, original task and scope change are in [TASK.json](TASK.json). + +## Credit and coordination + +**Arham Garg / @arhamgarg** owns PR2493's batching change, six tests, benchmark +and prior coverage argument. **Douglas Colkitt** and the overlap-tool contributors +own the original record schema, complete pagination and cache implementation; +**Nick Petalas** supplied the retained CLI-pagination compatibility work. +**Aaron Wang / PR2255** supplied the existing broader live-pin audit. +Their source headers and Apache-2.0 attribution are retained verbatim. + +Prepared for **Arham Tahir** with substantial **OpenAI Codex** assistance in +analysis, implementation, verification and writing. No subagents, independent +human review or formal-kernel result is claimed. New material is Apache-2.0. +The canonical investigation is [Discussion2789](https://github.com/CrocSwap/integer-mult-bounds/discussions/2789). +The initial scalar-to-moment assignment and explicit change to this side audit +are preserved in TASK.json; there is no paid-profile or kappa improvement claim. diff --git a/research/overlap-batching-independent-audit/RESULT.json b/research/overlap-batching-independent-audit/RESULT.json new file mode 100644 index 0000000000..17d53d9756 --- /dev/null +++ b/research/overlap-batching-independent-audit/RESULT.json @@ -0,0 +1,56 @@ +{ + "status": "PASS_OVERLAP_BATCHING_INDEPENDENT_AUDIT", + "parameter_cases": 864, + "static_graph_cases": 710, + "records_per_collector": 2062777, + "boundary_sizes": [ + 0, + 1, + 4, + 5, + 6, + 99, + 100, + 101, + 104, + 105, + 106, + 205 + ], + "thread_counts": [ + 0, + 1, + 49, + 50, + 51, + 101 + ], + "small_thread_requests": { + "baseline": 104, + "candidate": 3 + }, + "fault_injection": { + "baseline": { + "request_positions": 135, + "failure_cases": 810 + }, + "candidate": { + "request_positions": 84, + "failure_cases": 504 + } + }, + "source_corruptions_rejected": 3, + "truncation_mutants": { + "stop_comments": "ordered record mismatch", + "stop_replies": "ordered record mismatch", + "stop_listing": "ordered record mismatch" + }, + "predecessor_suite": { + "tests": 6, + "modes": [ + "normal", + "optimized" + ] + }, + "scope": "Finite static-API and cache-failure audit; no live timing, adoption or kappa claim." +} diff --git a/research/overlap-batching-independent-audit/SOURCES.json b/research/overlap-batching-independent-audit/SOURCES.json new file mode 100644 index 0000000000..2be96f3e8b --- /dev/null +++ b/research/overlap-batching-independent-audit/SOURCES.json @@ -0,0 +1,24 @@ +{ + "schema_version": 1, + "base_commit": "6ca6f217708b075814de409ad693fea9a2aaa760", + "files": [ + { + "path": "certificates/selected-result.json", + "usage": "provenance", + "git_blob": "ae6a99f28aa468649eb66a361e764d769363bf11", + "sha256": "10b9e340fd8e354a37a0c4813c90e5de1dd42044b5f6aca4a3fc5a42b685fa32" + }, + { + "path": "docs/research/agent-task-catalog.json", + "usage": "provenance", + "git_blob": "889b95d936746df124872133ae1e7cc339adcb92", + "sha256": "7d185cd8c2af35988d638d969748ad7b670f902606444dbfc748e4402c340df6" + }, + { + "path": "scripts/find_overlap.py", + "usage": "provenance", + "git_blob": "de0bca1f9e3ee60d0f1659a87af3869bae8c90da", + "sha256": "890902252573117253ecd536ea09a04115092c7255dfe2ebcf96f7e2d337fdbd" + } + ] +} diff --git a/research/overlap-batching-independent-audit/TASK.json b/research/overlap-batching-independent-audit/TASK.json new file mode 100644 index 0000000000..8f16ff4ede --- /dev/null +++ b/research/overlap-batching-independent-audit/TASK.json @@ -0,0 +1,134 @@ +{ + "schema_version": 1, + "task_id": "scalar-to-moment", + "priority_revision": 5, + "base_commit": "6ca6f217708b075814de409ad693fea9a2aaa760", + "original_card": { + "id": "scalar-to-moment", + "question": "Can one known scalar simplification be converted into an actual paid-profile improvement?", + "starting_points": [ + "research/r439-bsection-stars-seed01/README.md", + "certificates/selected-result.json" + ], + "methods": [ + "Derive the changed legal frame itinerary and exact child histogram.", + "Prove a scoped obstruction showing why that scalar gain cannot change the paid contract." + ], + "acceptance_gate": "An unchanged histogram is not a kappa gain; record any separate runtime or proof benefit explicitly.", + "first_step": { + "command": [ + "python3", + "-B", + "research/r439-bsection-stars-seed01/verify.py" + ], + "expected": "PASS_R439_BSECTION_STARS", + "inspect": "Observe that the positive control improves scalar counts while retaining the paid histogram.", + "scope": "Positive control / source-bound orientation only; this is not a new construction or full cold replay." + }, + "first_experiment": "Choose one simplification and derive the complete changed frame itinerary, or prove why it cannot change the histogram.", + "bottleneck_link": "A legal change to the histogram, not addition count alone, is the bridge to an exponent gain.", + "allowed_variations": [ + "One existing scalar simplification and one proposed frame-path change." + ], + "scope_change_triggers": [ + "Submitting only another scalar count as completion of a paid-profile task." + ], + "overlap_terms": [ + "scalar simplification", + "paid profile", + "R439" + ] + }, + "scope_status": "changed", + "actual_scope": "Independent finite static-API, complete-record and every-request-position cache-failure audit of PR2493 at a459ee34fad0488691beba8441e0a31e103b91a5.", + "scope_change_reason": "The scalar-to-moment positive control passed and nearby R439 frame questions were already covered. The mandatory live overlap scan exposed a coordination bottleneck already addressed by PR2493; the bounded contribution is independent verification of that existing proposal, not another batching implementation or mathematical mechanism.", + "bottleneck_link": "Side result in review tooling: independently test the complete-overlap evidence used to avoid duplicated research, including preservation of stale complete evidence on every injected page failure. This is not a paid-profile gain.", + "budget_minutes": 60, + "stop_rule": "One pinned collector proposal, a Cartesian threshold fixture, exhaustive request-position faults on one busy board, inherited tests, source controls and a public handoff. No collector edits, frozen-source repinning, cache redesign or family expansion.", + "outcome": "completed", + "evidence": "RESULT.json and verify.py: 710 distinct static graphs, 2062777 ordered records per collector, 1314 fault cases, three source corruptions and three truncation mutants; six predecessor tests normally and with -O.", + "author": "Arham Tahir", + "ai_assistance": "Substantial OpenAI Codex assistance in source inspection, harness implementation, verification and writing. No subagents or independent human review.", + "open_questions": [ + { + "question": "Can PR2493 be integrated with an explicit disposition for its two frozen dependent-package failures?", + "why_it_matters": "The source-bound collector passes this finite audit, but successful record collection does not resolve frozen provenance and live-source drift.", + "first_step": "Reproduce the two check-only failures listed in README.md, then record the maintainer-approved source-binding decision before collector adoption.", + "blocked_by": "Maintainer integration and source-binding policy; no authorization to edit frozen evidence is inferred." + } + ], + "selection_seed": "8e3fd61aeaea48140a3be0983b0943d3", + "selection_scope_key": "9dbfd943f23ef75bae31", + "track": "incumbent-optimization; explicit changed scope to independent tooling verification", + "discussion": "https://github.com/CrocSwap/integer-mult-bounds/discussions/2789", + "overlap": { + "entry_open_prs": 719, + "entry_discussion_records": 1425, + "open_prs_fetched_at": "2026-10-15T21:52:41.682513+00:00", + "discussions_fetched_at": "2026-10-15T21:55:43.603886+00:00", + "complete_pagination": true, + "nearest_work": [ + { + "pr": 2493, + "head": "a459ee34fad0488691beba8441e0a31e103b91a5", + "contributor": "Arham Garg / arhamgarg", + "added_value": "Independent threshold cross-product and all-request-position fault audit; batching implementation, six existing tests and measured speedup belong to this predecessor." + }, + { + "pr": 2255, + "head": "15836362a3cf17d245db8a01d930a12868c043bd", + "added_value": "Existing live-pin drift audit; this package does not repeat that broader analysis or resolve maintainer pin policy." + } + ], + "limits": "Titles/bodies, Discussion comments/replies and local package/ledger notes; not all code diffs. Independent verification intentionally overlaps the exact target." + }, + "freshness": { + "upstream_commit": "84f8e607779b81c05edc962d4981d1f9b4b9bc9f", + "recorded_at_utc": "2026-10-15T22:09:45.409937+00:00", + "priority_revision": 5, + "submission_parent": "84f8e607779b81c05edc962d4981d1f9b4b9bc9f", + "ancestry_check": "GitHub compare API reports ahead=8, behind=0 for 6ca6f217...84f8e607. Local shared repository is shallow; local merge-base could not establish this relationship.", + "compatibility": "Base 6ca6f217 remains an ancestor; AGENTS.md, autoresearch.md, catalogue, selected record, find_overlap.py and research_package.py unchanged at the fetched main. Only a new audit package is submitted; historical source pins retained.", + "target_pr": 2493, + "target_head": "a459ee34fad0488691beba8441e0a31e103b91a5", + "target_state": "open, ready; no review or Conversation comments at the direct check", + "overlap": { + "open_prs": { + "fetched_at": "2026-10-15T22:05:41.873096+00:00", + "complete_pagination": true, + "records": 722 + }, + "discussions": { + "fetched_at": "2026-10-15T22:06:03.800375+00:00", + "complete_pagination": true, + "records": 1436 + } + }, + "collector_note": "Final live overlap used the authenticated candidate collector after the offline audit passed; not a live timing comparison." + }, + "validation": { + "host": "Windows x64", + "python": [ + "CPython 3.12.14", + "CPython 3.14.6" + ], + "registered_replay": "CPython 3.12.14 normal and optimized commands passed, including six unchanged predecessor tests in both modes.", + "git_free_relocation": { + "mode": "Git-free path with spaces, unrelated cwd, -I -O -B", + "python": "Python 3.12.14", + "elapsed_seconds": 26.342, + "unchanged_files": 10 + }, + "known_rejections": { + "overlap-path-index": "source blob provenance mismatch: scripts/find_overlap.py", + "research-package-pin-batching": "working source drift: tests/test_research_onboarding.py" + }, + "not_claimed": [ + "Live timing ratio", + "Frozen dependent-package success", + "Independent human review", + "Hosted CI result", + "Supplier or selected cold replay" + ] + } +} diff --git a/research/overlap-batching-independent-audit/VERIFY.json b/research/overlap-batching-independent-audit/VERIFY.json new file mode 100644 index 0000000000..0993566fb8 --- /dev/null +++ b/research/overlap-batching-independent-audit/VERIFY.json @@ -0,0 +1,30 @@ +{ + "schema_version": 1, + "status": "ready", + "python": "3.12", + "requirements": [], + "inputs": [], + "commands": [ + { + "argv": [ + "{python}", + "-I", + "-B", + "{package}/verify.py" + ], + "timeout_seconds": 300, + "checks": "Pinned baseline/candidate vs direct records, 710 graphs, 1314 request-position faults, source/mutation controls and unchanged predecessor tests." + }, + { + "argv": [ + "{python}", + "-I", + "-O", + "-B", + "{package}/verify.py" + ], + "timeout_seconds": 300, + "checks": "Same complete audit under optimized Python; explicit checks remain active." + } + ] +} diff --git a/research/overlap-batching-independent-audit/sources/baseline.py b/research/overlap-batching-independent-audit/sources/baseline.py new file mode 100644 index 0000000000..de0bca1f9e --- /dev/null +++ b/research/overlap-batching-independent-audit/sources/baseline.py @@ -0,0 +1,377 @@ +#!/usr/bin/env python3 +"""Search PR bodies, discussion claims and local package READMEs for possible overlap. + +Standard library + optional authenticated gh CLI. Remote snapshots live outside +the checkout. Results are leads, not a novelty certificate. Douglas Colkitt, +with OpenAI Codex assistance. Apache-2.0. +""" +import argparse +from datetime import datetime, timezone +import fnmatch +import hashlib +import json +import os +from pathlib import Path, PurePosixPath +import subprocess +import sys +import tempfile +import time + +ROOT = Path(__file__).resolve().parents[1] +REPO = "CrocSwap/integer-mult-bounds" + + +def now(): + return datetime.now(timezone.utc).isoformat() + + +def gh_json(args, payload=None): + # --slurp is absent from older gh releases. Normalize it here so callers + # keep their nested-page contract. @json emits one compact array per page + # and escapes embedded newlines in PR titles and bodies. + paginated = "--paginate" in args + if paginated: + args = [arg for arg in args if arg != "--slurp"] + ["--jq", "@json"] + result = subprocess.run( + ["gh", "api", *args], input=None if payload is None else json.dumps(payload), + text=True, encoding="utf-8", capture_output=True, timeout=90, check=True, + ) + if paginated: + pages = [json.loads(line) for line in result.stdout.split("\n") if line] + if not pages or not all(isinstance(page, list) for page in pages): + raise ValueError("expected one JSON array per REST page") + return pages + value = json.loads(result.stdout) + if isinstance(value, dict) and value.get("errors"): + raise ValueError("GitHub GraphQL errors: " + json.dumps(value["errors"])) + return value + + +def graphql(query, variables): + return gh_json(["graphql", "--input", "-"], {"query": query, "variables": variables})["data"] + + +def pull_records(pages): + return [{"kind": "pr", "number": pr["number"], "title": pr["title"], + "body": pr.get("body") or "", "url": pr["html_url"], + "head": pr["head"]["sha"], "updated_at": pr["updated_at"], + "draft": pr.get("draft", False), "state": pr["state"]} + for page in pages for pr in page] + + +def fetch_pr_files(): + """Changed file paths per open PR, so overlap can be searched by path. + + GitHub reports at most 100 changed files per page; a PR beyond that is + flagged truncated. This is a file inventory, not a search of diff contents. + """ + rows = [] + cursor = None + while True: + # ponytail: 10 PRs per page (25x100-node pages exceed GitHub's ~10 s + # gateway limit and 502; 10x100 stays under it). A full index refresh is + # ~74 pages and only path queries pay it. Each page gets four attempts. + result = None + for attempt in range(4): + try: + result = graphql('''query($cursor:String) { + repository(owner:"CrocSwap",name:"integer-mult-bounds") { + pullRequests(first:10,states:OPEN,after:$cursor) { + pageInfo {hasNextPage endCursor} + nodes {number files(first:100) { + pageInfo {hasNextPage} + nodes {path} + }} + } + } + }''', {"cursor": cursor})["repository"]["pullRequests"] + break + except subprocess.SubprocessError: + if attempt == 3: + raise + time.sleep(1 + attempt) + for pull in result["nodes"]: + # PR 376 (and similar) report no file list; record it as empty and + # flagged so path search never silently trusts missing data. + connection = pull.get("files") or {"pageInfo": {"hasNextPage": True}, "nodes": []} + rows.append({"number": pull["number"], + "paths": sorted({node["path"] for node in connection["nodes"] if node}), + "truncated": bool(connection["pageInfo"]["hasNextPage"])}) + if not result["pageInfo"]["hasNextPage"]: + return sorted(rows, key=lambda row: row["number"]) + cursor = result["pageInfo"]["endCursor"] + + +def path_report(pr_records, file_rows, patterns): + """Open-PR entries whose changed files match any pattern (fnmatch or substring).""" + pulls = {record["number"]: record for record in pr_records if record.get("kind") == "pr"} + matches = [] + for row in sorted(file_rows, key=lambda item: -item["number"]): + matched = [path for path in row["paths"] + if any(fnmatch.fnmatch(path, pattern) or pattern in path for pattern in patterns)] + if not matched: + continue + pull = pulls.get(row["number"], {}) + matches.append({"kind": "pr", "number": row["number"], "title": pull.get("title", ""), + "url": pull.get("url") or f"https://github.com/{REPO}/pull/{row['number']}", + "head": pull.get("head"), + "updated_at": pull.get("updated_at"), "draft": pull.get("draft", False), + "matched_paths": matched, "changed_paths": len(row["paths"]), + "truncated": row.get("truncated", False)}) + return matches + + +def fetch_discussions(): + records = [] + cursor = None + while True: + result = graphql('''query($cursor:String) { + repository(owner:"CrocSwap",name:"integer-mult-bounds") { + discussions(first:50,after:$cursor) { + pageInfo {hasNextPage endCursor} + nodes {id number title body url updatedAt} + } + } + }''', {"cursor": cursor})["repository"]["discussions"] + for discussion in result["nodes"]: + common = {"number": discussion["number"], "title": discussion["title"]} + records.append({**common, "kind": "discussion", "body": discussion["body"], + "url": discussion["url"], "updated_at": discussion["updatedAt"]}) + comment_cursor = None + while True: + connection = graphql('''query($id:ID!, $cursor:String) { + node(id:$id) { ... on Discussion { + comments(first:100,after:$cursor) { + pageInfo {hasNextPage endCursor} + nodes {id body url updatedAt replies(first:100) { + pageInfo {hasNextPage endCursor} nodes {body url updatedAt} + }} + } + }} + }''', {"id": discussion["id"], "cursor": comment_cursor})["node"]["comments"] + for comment in connection["nodes"]: + records.append({**common, "kind": "discussion-comment", "body": comment["body"], + "url": comment["url"], "updated_at": comment["updatedAt"]}) + replies = comment["replies"] + while True: + for reply in replies["nodes"]: + records.append({**common, "kind": "discussion-reply", "body": reply["body"], + "url": reply["url"], "updated_at": reply["updatedAt"]}) + if not replies["pageInfo"]["hasNextPage"]: + break + replies = graphql('''query($id:ID!, $cursor:String) { + node(id:$id) { ... on DiscussionComment { + replies(first:100,after:$cursor) { + pageInfo {hasNextPage endCursor} nodes {body url updatedAt} + } + }} + }''', {"id": comment["id"], "cursor": replies["pageInfo"]["endCursor"]})["node"]["replies"] + if not connection["pageInfo"]["hasNextPage"]: + break + comment_cursor = connection["pageInfo"]["endCursor"] + if not result["pageInfo"]["hasNextPage"]: + return records + cursor = result["pageInfo"]["endCursor"] + + +def atomic_json(path, data): + path.parent.mkdir(parents=True, exist_ok=True) + with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", dir=path.parent, delete=False) as out: + tmp = Path(out.name) + json.dump(data, out) + out.write("\n") + try: + os.replace(tmp, path) + finally: + tmp.unlink(missing_ok=True) + + +def error_summary(exc): + if isinstance(exc, subprocess.CalledProcessError): + return (exc.stderr or str(exc))[:700] + return str(exc)[:700] + + +def remote_snapshot(cache, offline=False, refresh=False, max_age=300, need_files=False): + warnings = [] + try: + snapshot = json.loads(cache.read_text(encoding="utf-8")) + if snapshot.get("schema_version") != 1 or snapshot.get("repository") != REPO: + raise ValueError("cache schema or repository mismatch") + except (OSError, ValueError): + snapshot = {"schema_version": 1, "repository": REPO, "sources": {}} + sources = snapshot.setdefault("sources", {}) + fetches = [("open_prs", lambda: pull_records(gh_json([ + "--paginate", "--slurp", f"repos/{REPO}/pulls?state=open&per_page=100&sort=updated&direction=desc"]))), + ("discussions", fetch_discussions)] + if need_files: + # Only path queries pay for the changed-file index; body searches stay as they were. + fetches.append(("pr_files", fetch_pr_files)) + for name, fetch in fetches: + cached = sources.get(name) + age = None + if cached: + try: + age = (datetime.now(timezone.utc) - datetime.fromisoformat(cached["fetched_at"])).total_seconds() + except (KeyError, ValueError, TypeError): + cached = None + fresh = cached is not None and age is not None and 0 <= age <= max_age + if offline: + warnings.append(f"{name}: offline; cached data may miss current changes") + elif refresh or not fresh: + try: + # Replace each source only after every page succeeds. + records = fetch() + sources[name] = {"fetched_at": now(), "records": records, "complete_pagination": True} + atomic_json(cache, snapshot) + cached = sources[name] + except (OSError, ValueError, KeyError, TypeError, subprocess.SubprocessError) as exc: + warnings.append(f"{name}: refresh failed; using old data if available: {error_summary(exc)}") + if not cached: + warnings.append(f"{name}: no usable remote snapshot") + sources.pop(name, None) + return snapshot, warnings + + +def local_records(root): + records = [] + # Deliberately avoid vendored trees and large binary artifacts. + paths = sorted([*(root / "research").glob("*/README.md"), *(root / "research").glob("*/TASK.json")]) + ledger = root / "docs/research/pr-ledger.md" + if ledger.is_file(): + paths.append(ledger) + for path in paths: + if path.is_symlink(): + continue + data = path.read_bytes() + body = data.decode("utf-8", errors="replace") + name = path.relative_to(root).as_posix() + kind = {"README.md": "local-readme", "TASK.json": "local-task"}.get(path.name, "local-ledger") + records.append({"kind": kind, + "title": name, "path": name, "body": body, + "sha256": hashlib.sha256(data).hexdigest()}) + return records + + +def search(records, terms, all_terms=False): + terms = list(dict.fromkeys(t.strip().lower() for t in terms if t.strip())) + if not terms: + raise ValueError("provide at least one nonempty search term") + matches = [] + for record in records: + title, body = record["title"].lower(), record.get("body", "").lower() + found = [term for term in terms if term in title or term in body] + if not found or (all_terms and len(found) != len(terms)): + continue + positions = [body.find(term) for term in found if term in body] + start = max(0, min(positions) - 100) if positions else 0 + matches.append({**{k: v for k, v in record.items() if k != "body"}, + "matched_terms": found, + "score": sum(3 if t in title else 1 for t in found), + "excerpt": record.get("body", "")[start:start + 500]}) + return sorted(matches, key=lambda r: (-r["score"], r["kind"], r["title"], r.get("url", ""))) + + +def main(): + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("terms", nargs="*", help="literal case-insensitive terms; quote multiword phrases") + ap.add_argument("--task", help="add the named catalogue card's overlap terms") + ap.add_argument("--path", action="append", default=[], metavar="PATTERN", + help="match open PRs by changed file path (fnmatch pattern or substring); repeatable") + ap.add_argument("--offline", action="store_true") + ap.add_argument("--refresh", action="store_true") + ap.add_argument("--max-age", type=int, default=300, help="remote cache lifetime in seconds") + ap.add_argument("--limit", type=int, default=20) + ap.add_argument("--all-terms", action="store_true") + ap.add_argument("--json", action="store_true", dest="json_output") + ap.add_argument("--cache", type=Path, default=Path(os.environ.get("XDG_CACHE_HOME", str(Path.home() / ".cache"))) / "integer-mult-bounds/overlap.json") + args = ap.parse_args() + if args.max_age < 0 or args.limit < 1 or (args.offline and args.refresh): + ap.error("use nonnegative max-age, positive limit, and either offline or refresh") + if args.cache.resolve().is_relative_to(ROOT.resolve()): + ap.error("keep the cache outside the repository") + terms = list(args.terms) + if args.task: + catalog = json.loads((ROOT / "docs/research/agent-task-catalog.json").read_text(encoding="utf-8")) + cards = [t for tr in catalog["tracks"] for t in tr["tasks"] if t["id"] == args.task] + if not cards: + ap.error("unknown task") + terms.extend([args.task, *cards[0].get("overlap_terms", [])]) + # Catalogue paths are repository-relative POSIX paths. Nested sources + # identify their package, not generic directories such as evidence/code. + for point in cards[0]["starting_points"]: + parts = PurePosixPath(point).parts + if len(parts) >= 3 and parts[0] == "research": + terms.append(parts[1]) + if not any(t.strip() for t in terms) and not args.path: + ap.error("provide terms, --task or --path") + patterns = [p.strip() for p in args.path if p.strip()] + local = local_records(ROOT) + snapshot, warnings = remote_snapshot(args.cache, args.offline, args.refresh, args.max_age, + need_files=bool(patterns)) + records = list(local) + coverage = {} + for name in ("open_prs", "discussions"): + source = snapshot["sources"].get(name) + coverage[name] = {k: source[k] for k in ("fetched_at", "complete_pagination")} if source else None + if source: + records.extend(source["records"]) + coverage[name]["records"] = len(source["records"]) + path_matches = [] + if patterns: + source = snapshot["sources"].get("pr_files") + coverage["pr_files"] = {k: source[k] for k in ("fetched_at", "complete_pagination")} if source else None + if source: + coverage["pr_files"]["records"] = len(source["records"]) + pulls = snapshot["sources"].get("open_prs", {"records": []})["records"] + path_matches = path_report(pulls, source["records"], patterns) + matches = search(records, terms, args.all_terms) if any(t.strip() for t in terms) else [] + if patterns and matches: + # With both axes given, a record must match at least one term and one path. + hits = {m["number"]: m["matched_paths"] for m in path_matches} + matches = [dict(m, matched_paths=hits[m.get("number")]) for m in matches + if m.get("kind") == "pr" and m.get("number") in hits] + if patterns and not any(t.strip() for t in terms): + matches = [] + report = {"status": "POSSIBLE_OVERLAP_NOT_A_NOVELTY_CERTIFICATE", "searched_at": now(), + "terms": terms, "coverage": coverage, "local_records": len(local), "warnings": warnings, + "limits": "Remote PR scope is open PRs including drafts. Closed work is searched through local READMEs, TASK metadata and ledger only. No code diffs or arbitrary package files are searched. Recheck relevant heads before submission.", + "matches_total": len(matches), "matches": matches[:args.limit]} + if patterns: + report["path_patterns"] = patterns + report["path_limits"] = ("Path matching covers the changed file paths GitHub reports for open PRs, " + "not diff contents; PRs beyond 100 changed files are flagged truncated.") + report["path_matches_total"] = len(path_matches) + report["path_matches"] = path_matches[:args.limit] + if args.json_output: + print(json.dumps(report, indent=2)) + else: + # Excerpts quote arbitrary UTF-8; a cp1252 pipe must escape them, not crash. + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(errors="backslashreplace") + print(report["status"]) + print("Coverage: " + json.dumps(coverage)) + print(report["limits"]) + if patterns: + print(report["path_limits"]) + for warning in warnings: + print("WARNING: " + warning) + print(f"{len(matches)} possible matches; showing {min(args.limit, len(matches))}") + for match in matches[:args.limit]: + print(f"\n[{match['kind']}] {match['title']}\n{match.get('url', match.get('path'))}") + if match.get("head"): + print("Head: " + match["head"]) + print(match["excerpt"]) + if patterns: + print(f"\n{len(path_matches)} open PRs match paths {patterns}; " + f"showing {min(args.limit, len(path_matches))}") + for match in path_matches[:args.limit]: + flags = " (file list truncated)" if match["truncated"] else "" + print(f"\n[pr {match['number']}] {match['title']}{flags}\n{match['url']}") + if match.get("head"): + print("Head: " + match["head"]) + print("Matched paths: " + ", ".join(match["matched_paths"])) + + +if __name__ == "__main__": + main() diff --git a/research/overlap-batching-independent-audit/sources/candidate-tests.py b/research/overlap-batching-independent-audit/sources/candidate-tests.py new file mode 100644 index 0000000000..1ad26efd6e --- /dev/null +++ b/research/overlap-batching-independent-audit/sources/candidate-tests.py @@ -0,0 +1,185 @@ +"""Complete overlap records through nested GraphQL pagination and failures. + +Arham Garg, with substantial OpenAI Codex assistance. Apache-2.0. +""" +import copy +from contextlib import redirect_stdout +import importlib.util +import io +import json +from pathlib import Path +import re +import sys +import tempfile +import unittest +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location("overlap_batching", ROOT / "scripts/find_overlap.py") +overlap = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(overlap) + + +def node(ident): + return {"id": ident, "body": "Claim κ\n" + ident, + "url": "https://example.invalid/" + ident, "updatedAt": "2026-10-15T00:00:00Z"} + + +class Board: + """Serve a fixed graph using connection-specific opaque cursors. + + The transport honors the requested page sizes. Expected records come + directly from the graph, independently of the fetcher's pagination. + """ + def __init__(self, count=51, busy=False): + self.discussions = [] + self.comments = {} + self.replies = {} + self.calls = [] + self.fail = None + for i in range(count): + discussion = {**node(f"D{i}"), "number": i + 1, "title": f"Question {i}"} + self.discussions.append(discussion) + comments = [] + for j in range(207 if busy and i == 0 else i % 3): + comment = node(f"D{i}C{j}") + comments.append(comment) + n = 207 if busy and i == 0 and j in (0, 206) else j % 3 + self.replies[comment["id"]] = [node(f"D{i}C{j}R{k}") for k in range(n)] + self.comments[discussion["id"]] = comments + + @staticmethod + def page(items, count, cursor, owner): + if cursor is None: + start = 0 + else: + prefix, offset = cursor.rsplit(":", 1) + if prefix != owner: + raise ValueError("cursor belongs to another connection") + start = int(offset) + end = min(start + count, len(items)) + return {"nodes": copy.deepcopy(items[start:end]), + "pageInfo": {"hasNextPage": end < len(items), "endCursor": f"{owner}:{end}"}} + + def api(self, query, variables): + self.calls.append((query, dict(variables))) + if self.fail and self.fail(query, variables): + raise ValueError("injected later-page failure") + sizes = {name: int(size) for name, size in re.findall(r"(discussions|comments|replies)\(first:(\d+)", query)} + if "discussions" in sizes: + page = self.page(self.discussions, sizes["discussions"], variables["cursor"], "discussions") + if "comments" in sizes: + for discussion in page["nodes"]: + discussion["comments"] = self.comment_page(discussion["id"], None, sizes) + return {"repository": {"discussions": page}} + if "comments" in sizes: + return {"node": {"comments": self.comment_page(variables["id"], variables["cursor"], sizes)}} + return {"node": {"replies": self.page(self.replies[variables["id"]], sizes["replies"], + variables["cursor"], variables["id"] + "/replies")}} + + def comment_page(self, ident, cursor, sizes): + page = self.page(self.comments[ident], sizes["comments"], cursor, ident + "/comments") + for comment in page["nodes"]: + comment["replies"] = self.page(self.replies[comment["id"]], sizes["replies"], None, + comment["id"] + "/replies") + return page + + def expected(self): + records = [] + for discussion in self.discussions: + def append(kind, value): + records.append({"number": discussion["number"], "title": discussion["title"], + "kind": kind, "body": value["body"], "url": value["url"], + "updated_at": value["updatedAt"]}) + append("discussion", discussion) + for comment in self.comments[discussion["id"]]: + append("discussion-comment", comment) + for reply in self.replies[comment["id"]]: + append("discussion-reply", reply) + return records + + +class DiscussionBatchingTests(unittest.TestCase): + def test_small_threads_need_only_listing_pages(self): + board = Board() + with patch.object(overlap, "graphql", side_effect=board.api): + self.assertEqual(overlap.fetch_discussions(), board.expected()) + self.assertEqual(len(board.calls), 2, "small threads must share the two listing requests") + + def test_all_three_connections_paginate_without_losing_order_or_fields(self): + board = Board(busy=True) + with patch.object(overlap, "graphql", side_effect=board.api): + records = overlap.fetch_discussions() + self.assertEqual(records, board.expected()) + # The last comment and its last reply both lie beyond two page boundaries. + self.assertIn("Claim κ\nD0C206R206", [r["body"] for r in records]) + continuations = [v["cursor"] for _, v in board.calls if v["cursor"] is not None] + self.assertTrue(any(c.startswith("D0/comments:") for c in continuations)) + self.assertTrue(any(c.startswith("D0C206/replies:") for c in continuations)) + self.assertTrue(any(c.startswith("discussions:") for c in continuations)) + + def test_empty_board_is_a_complete_empty_list(self): + board = Board(count=0) + with patch.object(overlap, "graphql", side_effect=board.api): + self.assertEqual(overlap.fetch_discussions(), []) + self.assertEqual(len(board.calls), 1) + + def test_cli_finds_a_reply_beyond_all_page_boundaries(self): + board = Board(busy=True) + with tempfile.TemporaryDirectory() as tmp: + root, cache = Path(tmp) / "repo", Path(tmp) / "cache.json" + output = io.StringIO() + argv = ["find_overlap.py", "D0C206R206", "--json", "--cache", str(cache)] + with (patch.object(overlap, "ROOT", root), patch.object(sys, "argv", argv), + patch.object(overlap, "graphql", side_effect=board.api), + patch.object(overlap, "gh_json", return_value=[[]]), redirect_stdout(output)): + overlap.main() + report = json.loads(output.getvalue()) + self.assertEqual(report["warnings"], []) + self.assertEqual(report["coverage"]["discussions"]["records"], len(board.expected())) + self.assertTrue(report["coverage"]["discussions"]["complete_pagination"]) + self.assertEqual(report["matches_total"], 1) + match = report["matches"][0] + self.assertEqual(match["kind"], "discussion-reply") + self.assertEqual(match["url"], "https://example.invalid/D0C206R206") + self.assertEqual(match["excerpt"], "Claim κ\nD0C206R206") + self.assertEqual(json.loads(cache.read_text(encoding="utf-8"))["sources"]["discussions"]["records"], + board.expected()) + + def test_later_page_failures_preserve_the_old_complete_source(self): + selectors = ( + lambda q, v: v["cursor"] is not None and "discussions(first:" in q, + lambda q, v: v["cursor"] is not None and "comments(first:" in q, + lambda q, v: v.get("id") == "D0C206" and v["cursor"] is not None, + ) + with tempfile.TemporaryDirectory() as tmp: + cache = Path(tmp) / "cache.json" + board = Board(busy=True) + with (patch.object(overlap, "graphql", side_effect=board.api), + patch.object(overlap, "gh_json", return_value=[[]])): + original, warnings = overlap.remote_snapshot(cache) + self.assertEqual(warnings, []) + for selector in selectors: + with self.subTest(selector=selector): + board.fail = selector + with (patch.object(overlap, "graphql", side_effect=board.api), + patch.object(overlap, "gh_json", return_value=[[]])): + current, warnings = overlap.remote_snapshot(cache, refresh=True) + self.assertTrue(any("discussions: refresh failed" in w for w in warnings)) + self.assertEqual(current["sources"]["discussions"], original["sources"]["discussions"]) + self.assertEqual(json.loads(cache.read_text(encoding="utf-8"))["sources"]["discussions"], + original["sources"]["discussions"]) + + def test_first_failed_scan_does_not_publish_partial_records(self): + board = Board(busy=True) + board.fail = lambda q, v: v.get("id") == "D0C206" and v["cursor"] is not None + with tempfile.TemporaryDirectory() as tmp: + with (patch.object(overlap, "graphql", side_effect=board.api), + patch.object(overlap, "gh_json", return_value=[[]])): + snapshot, warnings = overlap.remote_snapshot(Path(tmp) / "cache.json") + self.assertNotIn("discussions", snapshot["sources"]) + self.assertTrue(any("discussions: no usable remote snapshot" in w for w in warnings)) + + +if __name__ == "__main__": + unittest.main() diff --git a/research/overlap-batching-independent-audit/sources/candidate.py b/research/overlap-batching-independent-audit/sources/candidate.py new file mode 100644 index 0000000000..5bdee10f1a --- /dev/null +++ b/research/overlap-batching-independent-audit/sources/candidate.py @@ -0,0 +1,378 @@ +#!/usr/bin/env python3 +"""Search PR bodies, discussion claims and local package READMEs for possible overlap. + +Standard library + optional authenticated gh CLI. Remote snapshots live outside +the checkout. Results are leads, not a novelty certificate. Douglas Colkitt, +with OpenAI Codex assistance. Apache-2.0. +""" +import argparse +from datetime import datetime, timezone +import fnmatch +import hashlib +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import time + +ROOT = Path(__file__).resolve().parents[1] +REPO = "CrocSwap/integer-mult-bounds" + + +def now(): + return datetime.now(timezone.utc).isoformat() + + +def gh_json(args, payload=None): + # --slurp is absent from older gh releases. Normalize it here so callers + # keep their nested-page contract. @json emits one compact array per page + # and escapes embedded newlines in PR titles and bodies. + paginated = "--paginate" in args + if paginated: + args = [arg for arg in args if arg != "--slurp"] + ["--jq", "@json"] + result = subprocess.run( + ["gh", "api", *args], input=None if payload is None else json.dumps(payload), + text=True, encoding="utf-8", capture_output=True, timeout=90, check=True, + ) + if paginated: + pages = [json.loads(line) for line in result.stdout.split("\n") if line] + if not pages or not all(isinstance(page, list) for page in pages): + raise ValueError("expected one JSON array per REST page") + return pages + value = json.loads(result.stdout) + if isinstance(value, dict) and value.get("errors"): + raise ValueError("GitHub GraphQL errors: " + json.dumps(value["errors"])) + return value + + +def graphql(query, variables): + return gh_json(["graphql", "--input", "-"], {"query": query, "variables": variables})["data"] + + +def pull_records(pages): + return [{"kind": "pr", "number": pr["number"], "title": pr["title"], + "body": pr.get("body") or "", "url": pr["html_url"], + "head": pr["head"]["sha"], "updated_at": pr["updated_at"], + "draft": pr.get("draft", False), "state": pr["state"]} + for page in pages for pr in page] + + +def fetch_pr_files(): + """Changed file paths per open PR, so overlap can be searched by path. + + GitHub reports at most 100 changed files per page; a PR beyond that is + flagged truncated. This is a file inventory, not a search of diff contents. + """ + rows = [] + cursor = None + while True: + # ponytail: 10 PRs per page (25x100-node pages exceed GitHub's ~10 s + # gateway limit and 502; 10x100 stays under it). A full index refresh is + # ~74 pages and only path queries pay it. Each page gets four attempts. + result = None + for attempt in range(4): + try: + result = graphql('''query($cursor:String) { + repository(owner:"CrocSwap",name:"integer-mult-bounds") { + pullRequests(first:10,states:OPEN,after:$cursor) { + pageInfo {hasNextPage endCursor} + nodes {number files(first:100) { + pageInfo {hasNextPage} + nodes {path} + }} + } + } + }''', {"cursor": cursor})["repository"]["pullRequests"] + break + except subprocess.SubprocessError: + if attempt == 3: + raise + time.sleep(1 + attempt) + for pull in result["nodes"]: + # PR 376 (and similar) report no file list; record it as empty and + # flagged so path search never silently trusts missing data. + connection = pull.get("files") or {"pageInfo": {"hasNextPage": True}, "nodes": []} + rows.append({"number": pull["number"], + "paths": sorted({node["path"] for node in connection["nodes"] if node}), + "truncated": bool(connection["pageInfo"]["hasNextPage"])}) + if not result["pageInfo"]["hasNextPage"]: + return sorted(rows, key=lambda row: row["number"]) + cursor = result["pageInfo"]["endCursor"] + + +def path_report(pr_records, file_rows, patterns): + """Open-PR entries whose changed files match any pattern (fnmatch or substring).""" + pulls = {record["number"]: record for record in pr_records if record.get("kind") == "pr"} + matches = [] + for row in sorted(file_rows, key=lambda item: -item["number"]): + matched = [path for path in row["paths"] + if any(fnmatch.fnmatch(path, pattern) or pattern in path for pattern in patterns)] + if not matched: + continue + pull = pulls.get(row["number"], {}) + matches.append({"kind": "pr", "number": row["number"], "title": pull.get("title", ""), + "url": pull.get("url") or f"https://github.com/{REPO}/pull/{row['number']}", + "head": pull.get("head"), + "updated_at": pull.get("updated_at"), "draft": pull.get("draft", False), + "matched_paths": matched, "changed_paths": len(row["paths"]), + "truncated": row.get("truncated", False)}) + return matches + + +def fetch_discussions(): + records = [] + cursor = None + while True: + result = graphql('''query($cursor:String) { + repository(owner:"CrocSwap",name:"integer-mult-bounds") { + discussions(first:50,after:$cursor) { + pageInfo {hasNextPage endCursor} + nodes {id number title body url updatedAt comments(first:5) { + pageInfo {hasNextPage endCursor} + nodes {id body url updatedAt replies(first:5) { + pageInfo {hasNextPage endCursor} nodes {body url updatedAt} + }} + }} + } + } + }''', {"cursor": cursor})["repository"]["discussions"] + for discussion in result["nodes"]: + common = {"number": discussion["number"], "title": discussion["title"]} + records.append({**common, "kind": "discussion", "body": discussion["body"], + "url": discussion["url"], "updated_at": discussion["updatedAt"]}) + # Small threads share the listing request. Keep the nested first + # pages bounded, then exhaust each continuation for busy hubs. + connection = discussion["comments"] + while True: + for comment in connection["nodes"]: + records.append({**common, "kind": "discussion-comment", "body": comment["body"], + "url": comment["url"], "updated_at": comment["updatedAt"]}) + replies = comment["replies"] + while True: + for reply in replies["nodes"]: + records.append({**common, "kind": "discussion-reply", "body": reply["body"], + "url": reply["url"], "updated_at": reply["updatedAt"]}) + if not replies["pageInfo"]["hasNextPage"]: + break + replies = graphql('''query($id:ID!, $cursor:String) { + node(id:$id) { ... on DiscussionComment { + replies(first:100,after:$cursor) { + pageInfo {hasNextPage endCursor} nodes {body url updatedAt} + } + }} + }''', {"id": comment["id"], "cursor": replies["pageInfo"]["endCursor"]})["node"]["replies"] + if not connection["pageInfo"]["hasNextPage"]: + break + connection = graphql('''query($id:ID!, $cursor:String) { + node(id:$id) { ... on Discussion { + comments(first:100,after:$cursor) { + pageInfo {hasNextPage endCursor} + nodes {id body url updatedAt replies(first:100) { + pageInfo {hasNextPage endCursor} nodes {body url updatedAt} + }} + } + }} + }''', {"id": discussion["id"], "cursor": connection["pageInfo"]["endCursor"]})["node"]["comments"] + if not result["pageInfo"]["hasNextPage"]: + return records + cursor = result["pageInfo"]["endCursor"] + + +def atomic_json(path, data): + path.parent.mkdir(parents=True, exist_ok=True) + with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", dir=path.parent, delete=False) as out: + tmp = Path(out.name) + json.dump(data, out) + out.write("\n") + try: + os.replace(tmp, path) + finally: + tmp.unlink(missing_ok=True) + + +def error_summary(exc): + if isinstance(exc, subprocess.CalledProcessError): + return (exc.stderr or str(exc))[:700] + return str(exc)[:700] + + +def remote_snapshot(cache, offline=False, refresh=False, max_age=300, need_files=False): + warnings = [] + try: + snapshot = json.loads(cache.read_text(encoding="utf-8")) + if snapshot.get("schema_version") != 1 or snapshot.get("repository") != REPO: + raise ValueError("cache schema or repository mismatch") + except (OSError, ValueError): + snapshot = {"schema_version": 1, "repository": REPO, "sources": {}} + sources = snapshot.setdefault("sources", {}) + fetches = [("open_prs", lambda: pull_records(gh_json([ + "--paginate", "--slurp", f"repos/{REPO}/pulls?state=open&per_page=100&sort=updated&direction=desc"]))), + ("discussions", fetch_discussions)] + if need_files: + # Only path queries pay for the changed-file index; body searches stay as they were. + fetches.append(("pr_files", fetch_pr_files)) + for name, fetch in fetches: + cached = sources.get(name) + age = None + if cached: + try: + age = (datetime.now(timezone.utc) - datetime.fromisoformat(cached["fetched_at"])).total_seconds() + except (KeyError, ValueError, TypeError): + cached = None + fresh = cached is not None and age is not None and 0 <= age <= max_age + if offline: + warnings.append(f"{name}: offline; cached data may miss current changes") + elif refresh or not fresh: + try: + # Replace each source only after every page succeeds. + records = fetch() + sources[name] = {"fetched_at": now(), "records": records, "complete_pagination": True} + atomic_json(cache, snapshot) + cached = sources[name] + except (OSError, ValueError, KeyError, TypeError, subprocess.SubprocessError) as exc: + warnings.append(f"{name}: refresh failed; using old data if available: {error_summary(exc)}") + if not cached: + warnings.append(f"{name}: no usable remote snapshot") + sources.pop(name, None) + return snapshot, warnings + + +def local_records(root): + records = [] + # Deliberately avoid vendored trees and large binary artifacts. + paths = sorted([*(root / "research").glob("*/README.md"), *(root / "research").glob("*/TASK.json")]) + ledger = root / "docs/research/pr-ledger.md" + if ledger.is_file(): + paths.append(ledger) + for path in paths: + if path.is_symlink(): + continue + data = path.read_bytes() + body = data.decode("utf-8", errors="replace") + name = path.relative_to(root).as_posix() + kind = {"README.md": "local-readme", "TASK.json": "local-task"}.get(path.name, "local-ledger") + records.append({"kind": kind, + "title": name, "path": name, "body": body, + "sha256": hashlib.sha256(data).hexdigest()}) + return records + + +def search(records, terms, all_terms=False): + terms = list(dict.fromkeys(t.strip().lower() for t in terms if t.strip())) + if not terms: + raise ValueError("provide at least one nonempty search term") + matches = [] + for record in records: + title, body = record["title"].lower(), record.get("body", "").lower() + found = [term for term in terms if term in title or term in body] + if not found or (all_terms and len(found) != len(terms)): + continue + positions = [body.find(term) for term in found if term in body] + start = max(0, min(positions) - 100) if positions else 0 + matches.append({**{k: v for k, v in record.items() if k != "body"}, + "matched_terms": found, + "score": sum(3 if t in title else 1 for t in found), + "excerpt": record.get("body", "")[start:start + 500]}) + return sorted(matches, key=lambda r: (-r["score"], r["kind"], r["title"], r.get("url", ""))) + + +def main(): + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("terms", nargs="*", help="literal case-insensitive terms; quote multiword phrases") + ap.add_argument("--task", help="add the named catalogue card's overlap terms") + ap.add_argument("--path", action="append", default=[], metavar="PATTERN", + help="match open PRs by changed file path (fnmatch pattern or substring); repeatable") + ap.add_argument("--offline", action="store_true") + ap.add_argument("--refresh", action="store_true") + ap.add_argument("--max-age", type=int, default=300, help="remote cache lifetime in seconds") + ap.add_argument("--limit", type=int, default=20) + ap.add_argument("--all-terms", action="store_true") + ap.add_argument("--json", action="store_true", dest="json_output") + ap.add_argument("--cache", type=Path, default=Path(os.environ.get("XDG_CACHE_HOME", str(Path.home() / ".cache"))) / "integer-mult-bounds/overlap.json") + args = ap.parse_args() + if args.max_age < 0 or args.limit < 1 or (args.offline and args.refresh): + ap.error("use nonnegative max-age, positive limit, and either offline or refresh") + if args.cache.resolve().is_relative_to(ROOT.resolve()): + ap.error("keep the cache outside the repository") + terms = list(args.terms) + if args.task: + catalog = json.loads((ROOT / "docs/research/agent-task-catalog.json").read_text(encoding="utf-8")) + cards = [t for tr in catalog["tracks"] for t in tr["tasks"] if t["id"] == args.task] + if not cards: + ap.error("unknown task") + terms.extend([args.task, *cards[0].get("overlap_terms", [])]) + terms.extend(Path(p).parent.name for p in cards[0]["starting_points"] if p.startswith("research/")) + if not any(t.strip() for t in terms) and not args.path: + ap.error("provide terms, --task or --path") + patterns = [p.strip() for p in args.path if p.strip()] + local = local_records(ROOT) + snapshot, warnings = remote_snapshot(args.cache, args.offline, args.refresh, args.max_age, + need_files=bool(patterns)) + records = list(local) + coverage = {} + for name in ("open_prs", "discussions"): + source = snapshot["sources"].get(name) + coverage[name] = {k: source[k] for k in ("fetched_at", "complete_pagination")} if source else None + if source: + records.extend(source["records"]) + coverage[name]["records"] = len(source["records"]) + path_matches = [] + if patterns: + source = snapshot["sources"].get("pr_files") + coverage["pr_files"] = {k: source[k] for k in ("fetched_at", "complete_pagination")} if source else None + if source: + coverage["pr_files"]["records"] = len(source["records"]) + pulls = snapshot["sources"].get("open_prs", {"records": []})["records"] + path_matches = path_report(pulls, source["records"], patterns) + matches = search(records, terms, args.all_terms) if any(t.strip() for t in terms) else [] + if patterns and matches: + # With both axes given, a record must match at least one term and one path. + hits = {m["number"]: m["matched_paths"] for m in path_matches} + matches = [dict(m, matched_paths=hits[m.get("number")]) for m in matches + if m.get("kind") == "pr" and m.get("number") in hits] + if patterns and not any(t.strip() for t in terms): + matches = [] + report = {"status": "POSSIBLE_OVERLAP_NOT_A_NOVELTY_CERTIFICATE", "searched_at": now(), + "terms": terms, "coverage": coverage, "local_records": len(local), "warnings": warnings, + "limits": "Remote PR scope is open PRs including drafts. Closed work is searched through local READMEs, TASK metadata and ledger only. No code diffs or arbitrary package files are searched. Recheck relevant heads before submission.", + "matches_total": len(matches), "matches": matches[:args.limit]} + if patterns: + report["path_patterns"] = patterns + report["path_limits"] = ("Path matching covers the changed file paths GitHub reports for open PRs, " + "not diff contents; PRs beyond 100 changed files are flagged truncated.") + report["path_matches_total"] = len(path_matches) + report["path_matches"] = path_matches[:args.limit] + if args.json_output: + print(json.dumps(report, indent=2)) + else: + # Excerpts quote arbitrary UTF-8; a cp1252 pipe must escape them, not crash. + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(errors="backslashreplace") + print(report["status"]) + print("Coverage: " + json.dumps(coverage)) + print(report["limits"]) + if patterns: + print(report["path_limits"]) + for warning in warnings: + print("WARNING: " + warning) + print(f"{len(matches)} possible matches; showing {min(args.limit, len(matches))}") + for match in matches[:args.limit]: + print(f"\n[{match['kind']}] {match['title']}\n{match.get('url', match.get('path'))}") + if match.get("head"): + print("Head: " + match["head"]) + print(match["excerpt"]) + if patterns: + print(f"\n{len(path_matches)} open PRs match paths {patterns}; " + f"showing {min(args.limit, len(path_matches))}") + for match in path_matches[:args.limit]: + flags = " (file list truncated)" if match["truncated"] else "" + print(f"\n[pr {match['number']}] {match['title']}{flags}\n{match['url']}") + if match.get("head"): + print("Head: " + match["head"]) + print("Matched paths: " + ", ".join(match["matched_paths"])) + + +if __name__ == "__main__": + main() diff --git a/research/overlap-batching-independent-audit/verify.py b/research/overlap-batching-independent-audit/verify.py new file mode 100644 index 0000000000..d6eb23e544 --- /dev/null +++ b/research/overlap-batching-independent-audit/verify.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python3 +"""Independent finite API/failure audit of Arham Garg's PR2493. + +Arham Tahir, with substantial OpenAI Codex assistance. Apache-2.0. +Only authenticated source bytes execute; no network or Git is required. +""" +import ast +import base64 +import copy +import hashlib +import itertools +import json +from pathlib import Path +import re +import subprocess +import sys +import tempfile +import types + +HERE = Path(__file__).resolve().parent + + +def require(condition, message): + if not condition: + raise ValueError(message) + + +def authenticated_sources(directory=HERE): + records = json.loads((directory / 'DEPENDENCIES.json').read_text(encoding='utf-8')) + answer = {} + # Authenticate every input before compiling any of them. + for record in records: + data = (directory / record['local_path']).read_bytes() + require(hashlib.sha256(data).hexdigest() == record['sha256'], 'source SHA mismatch') + blob = b'blob ' + str(len(data)).encode() + b'\0' + data + require(hashlib.sha1(blob).hexdigest() == record['git_blob'], 'source blob mismatch') + answer[Path(record['local_path']).name] = data + require(set(answer) == {'baseline.py', 'candidate.py', 'candidate-tests.py'}, 'source inventory') + return answer + + +def load(data, name): + module = types.ModuleType(name) + module.__file__ = str(HERE / 'sources' / (name + '.py')) + exec(compile(data, module.__file__, 'exec'), module.__dict__) + return module + + +def item(identity): + return {'id':identity, 'body':'Repeated claim κ\n\u0085\u2028\u2029', + 'url':'https://example.invalid/' + identity, 'updatedAt':'2026-10-15T21:00:00Z'} + + +def corpus(thread_count, comment_count, reply_count): + """Replies exercise both the embedded and subsequent comment pages.""" + threads = [] + for i in range(thread_count): + thread = dict(item('thread-' + str(i)), number=1000+i, title='Question ' + str(i)) + thread['children'] = [] + # A second busy thread detects a wrongly reused connection cursor. + for j in range(comment_count if i in (0, thread_count-1) else i % 3): + comment = item(f'thread-{i}-comment-{j}') + n = reply_count if j in (0, 5, comment_count-1) else j % 2 + comment['children'] = [item(f'thread-{i}-comment-{j}-reply-{k}') for k in range(n)] + thread['children'].append(comment) + threads.append(thread) + return threads + + +def direct_records(threads): + """No cursors, API page sizes, or collector code enter this oracle.""" + result = [] + for thread in threads: + def emit(kind, node): + result.append({'kind':kind, 'number':thread['number'], 'title':thread['title'], + 'body':node['body'], 'url':node['url'], 'updated_at':node['updatedAt']}) + emit('discussion', thread) + for comment in thread['children']: + emit('discussion-comment', comment) + for reply in comment['children']: + emit('discussion-reply', reply) + return result + + +class API: + def __init__(self, threads, fail_at=None, failure='graphql'): + self.threads = threads + self.nodes = {node['id']:node for thread in threads + for node in [thread, *thread['children']]} + self.calls = [] + self.fail_at, self.failure = fail_at, failure + + @staticmethod + def page(nodes, size, cursor, owner): + start = 0 + if cursor is not None: + decoded_owner, start = json.loads(base64.b64decode(cursor, validate=True)) + require(decoded_owner == owner and 0 < start <= len(nodes), 'wrong connection cursor') + end = min(start+size, len(nodes)) + opaque = base64.b64encode(json.dumps([owner, end]).encode()).decode() if end else None + return {'nodes':[{k:v for k,v in node.items() if k != 'children'} + for node in nodes[start:end]], + 'pageInfo':{'hasNextPage':end < len(nodes), 'endCursor':opaque}} + + def comments(self, thread, size, reply_size, cursor): + page = self.page(thread['children'], size, cursor, thread['id'] + ':comments') + for node in page['nodes']: + node['replies'] = self.page(self.nodes[node['id']]['children'], reply_size, None, + node['id'] + ':replies') + return page + + def __call__(self, query, variables): + self.calls.append((query, copy.deepcopy(variables))) + if len(self.calls) == self.fail_at: + if self.failure == 'transport': + raise OSError('injected transport failure') + if self.failure == 'malformed': + return {} # Required connection absent: no partial cache admission. + raise ValueError('GitHub GraphQL errors: injected page failure') + sizes = dict((name,int(size)) for name,size in re.findall( + r'(discussions|comments|replies)\(first:(\d+)', query)) + require(sizes and all(0 < n <= 100 for n in sizes.values()), 'unexpected query') + cursor = variables['cursor'] + if 'discussions' in sizes: + result = self.page(self.threads, sizes['discussions'], cursor, 'listing') + if 'comments' in sizes: + for node in result['nodes']: + node['comments'] = self.comments(self.nodes[node['id']], sizes['comments'], + sizes['replies'], None) + return {'repository':{'discussions':result}} + node = self.nodes[variables['id']] + if 'comments' in sizes: + result = self.comments(node, sizes['comments'], sizes['replies'], cursor) + return {'node':{'comments':result}} + return {'node':{'replies':self.page(node['children'], sizes['replies'], cursor, + node['id'] + ':replies')}} + + +def collect(module, threads): + api = API(threads) + module.graphql = api + records = module.fetch_discussions() + require(records == direct_records(threads), 'ordered record mismatch') + return len(api.calls) + + +def failure_checks(module, threads, work): + request_count = collect(module, threads) + previous = {'fetched_at':'2020-01-02T03:04:05+00:00', 'complete_pagination':True, + 'records':[{'kind':'discussion', 'body':'Previously complete sentinel'}]} + cases = 0 + for fail_at, failure, existing in itertools.product( + range(1, request_count+1), ('graphql','transport','malformed'), (False,True)): + cache = work / 'cache.json' + cache.unlink(missing_ok=True) + if existing: + cache.write_text(json.dumps({'schema_version':1, + 'repository':'CrocSwap/integer-mult-bounds', + 'sources':{'discussions':previous}}), encoding='utf-8') + module.graphql = API(threads, fail_at, failure) + module.gh_json = lambda *args, **kwargs: [[]] + snapshot, warnings = module.remote_snapshot(cache, refresh=True) + stored = json.loads(cache.read_text(encoding='utf-8')) + require(any('discussions: refresh failed' in w for w in warnings), 'missing failure warning') + require(len(module.graphql.calls) == fail_at, 'failure injection missed') + for observed in (snapshot, stored): + if existing: + require(observed['sources']['discussions'] == previous, 'changed complete source or timestamp') + else: + require('discussions' not in observed['sources'], 'partial first-fetch source admitted') + cases += 1 + return {'request_positions':request_count, 'failure_cases':cases} + + +def corruption_controls(sources, work): + isolated = work / 'corrupted' + isolated.mkdir() + (isolated / 'sources').mkdir() + (isolated / 'DEPENDENCIES.json').write_bytes((HERE / 'DEPENDENCIES.json').read_bytes()) + for name, data in sources.items(): + (isolated / 'sources' / name).write_bytes(data) + for name, data in sources.items(): + # This would execute a visible failure if authentication happened too late. + (isolated / 'sources' / name).write_bytes(data + b'\nraise RuntimeError("executed corrupt source")\n') + try: + authenticated_sources(isolated) + except ValueError as error: + require(str(error) == 'source SHA mismatch', 'unexpected source rejection') + else: + raise ValueError('corrupt source admitted') + (isolated / 'sources' / name).write_bytes(data) + return len(sources) + + +def inherited_tests(sources, work): + root = work / 'predecessor-tests' + (root / 'scripts').mkdir(parents=True) + (root / 'tests').mkdir() + script = root / 'tests/test_research_discussion_batching.py' + script.write_bytes(sources['candidate-tests.py']) + (root / 'scripts/find_overlap.py').write_bytes(sources['candidate.py']) + for options in ([], ['-O']): + result = subprocess.run([sys.executable, '-I', *options, '-B', str(script)], + capture_output=True, text=True, encoding='utf-8', timeout=60, cwd=root) + require(result.returncode == 0 and 'Ran 6 tests' in result.stderr, + 'predecessor suite failed: ' + result.stderr[-2000:]) + return {'tests':6, 'modes':['normal','optimized']} + + +def run(): + sources = authenticated_sources() + modules = {name:load(sources[name + '.py'],name) for name in ('baseline','candidate')} + thresholds = (0,1,4,5,6,99,100,101,104,105,106,205) + cases = records = parameter_cases = 0 + seen = set() + for n,c,r in itertools.product((0,1,49,50,51,101), thresholds, thresholds): + parameter_cases += 1 + threads = corpus(n,c,r) + digest = hashlib.sha256(json.dumps(threads,sort_keys=True).encode()).hexdigest() + if digest in seen: + continue + seen.add(digest) + for module in modules.values(): + collect(module, threads) + cases += 1 + records += len(direct_records(threads)) + small = corpus(101,4,4) + request_counts = {name:collect(module,small) for name,module in modules.items()} + require(request_counts == {'baseline':104,'candidate':3}, 'small-thread request count') + busy = corpus(51,205,205) + with tempfile.TemporaryDirectory(prefix='overlap-independent-') as tmp: + work = Path(tmp) + failures = {name:failure_checks(module,busy,work) for name,module in modules.items()} + source_controls = corruption_controls(sources,work) + inherited = inherited_tests(sources,work) + mutants = {} + for label, expression in ( + ('stop_comments','connection'), ('stop_replies','replies'), ('stop_listing','result')): + before = f'if not {expression}["pageInfo"]["hasNextPage"]:' + code = sources['candidate.py'].decode('utf-8') + function = next(node for node in ast.parse(code).body + if isinstance(node,ast.FunctionDef) and node.name == 'fetch_discussions') + lines = code.splitlines(keepends=True) + body = ''.join(lines[function.lineno-1:function.end_lineno]) + require(body.count(before) == 1, 'mutation site changed') + lines[function.lineno-1:function.end_lineno] = [body.replace(before,'if True:')] + mutant = load(''.join(lines).encode(),'mutant') + try: + collect(mutant,busy) + except ValueError as error: + require(str(error) == 'ordered record mismatch', 'unexpected mutant rejection') + mutants[label] = 'ordered record mismatch' + else: + raise ValueError('truncation mutant accepted') + return {'status':'PASS_OVERLAP_BATCHING_INDEPENDENT_AUDIT', + 'parameter_cases':parameter_cases,'static_graph_cases':cases,'records_per_collector':records, + 'boundary_sizes':list(thresholds),'thread_counts':[0,1,49,50,51,101], + 'small_thread_requests':request_counts,'fault_injection':failures, + 'source_corruptions_rejected':source_controls,'truncation_mutants':mutants, + 'predecessor_suite':inherited, + 'scope':'Finite static-API and cache-failure audit; no live timing, adoption or kappa claim.'} + + +if __name__ == '__main__': + result = run() + if sys.argv[1:] == ['--write']: + (HERE / 'RESULT.json').write_text(json.dumps(result,indent=2) + '\n', encoding='utf-8') + else: + require(not sys.argv[1:], 'unknown argument') + expected = json.loads((HERE / 'RESULT.json').read_text(encoding='utf-8')) + require(json.dumps(result,sort_keys=True) == json.dumps(expected,sort_keys=True), 'receipt mismatch') + print(json.dumps(result,indent=2))