Skip to content

perf: repo-finder burns ~59-109 API calls per scan for data it already fetched#28

Merged
gaurav0107 merged 4 commits into
gaurav0107:mainfrom
imkp1:feat/repo-finder-query-shape
Jul 13, 2026
Merged

perf: repo-finder burns ~59-109 API calls per scan for data it already fetched#28
gaurav0107 merged 4 commits into
gaurav0107:mainfrom
imkp1:feat/repo-finder-query-shape

Conversation

@imkp1

@imkp1 imkp1 commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Problem

A single /repo-finder scan makes ~59-109 GitHub API requests. Almost all of them are redundant.

Step 1 runs nine searches. Step 2 then loops over the 50-100 surviving candidates and, for each one, calls:

gh api repos/OWNER/REPO --jq "[.pushed_at, .open_issues_count, .archived, .default_branch] | @tsv"

All four of those fields ship inline in every search result. Step 1 fetched them, discarded them, and Step 2 buys them back one repo at a time. The existing comment (# One repo call, not three.) shows this was already optimized 3 calls -> 1. It can go 1 -> 0.

Three of the nine searches are also junk: "spring boot" and "fastapi" are free-text matches against the name/description with no language or topic check, and the bare --stars ">20000" catch-all admits any repo above the star floor in any language, tagged or not. These are the only paths by which a repo matching no criterion enters the pool.

What changed

Search via gh api search/repositories. The porcelain cannot return topics[] at any price, and topics[] is what lets the agent justify a ranking instead of asserting one. It now reaches the shortlist row. The same projection returns the four fields Step 2 was re-fetching.

Delete the per-candidate repo call. $PUSHED_AT, $OPEN_ISSUES, $DEFAULT_BRANCH are read from the record already in hand. The archived check becomes dead code once archived:false is a server-side qualifier. The 30-day merged-PR check (gh pr list --search) stays — no search result carries that signal.

Nine queries -> five. Repeated qualifiers do not behave alike, and this is load-bearing:

Qualifier Repeated behavior
language: ORlanguage:go language:rust returns Go ∪ Rust
topic: ANDtopic:cli topic:terminal returns only repos tagged both

So java and python union into one request (was two), but multi-topic-OR is impossible in one query. A second topic: silently intersects — smaller or empty set, no error. Query count is |topics|, not |langs| × |topics|. The agent now states this as a rule.

Also: sort=stars instead of updated (which reordered hourly, leaving the per-query cutoff non-deterministic and nothing downstream testable), and archived:false server-side so dead repos never consume a result slot.

Silent-failure surface this closes

Unrecognized qualifiers are not errors. langauge:go is parsed as free text and returns total_count: 0 with exit 0 — the same fail-silent signature as the OR/parens bug fixed in #25. The agent now constrains every key: token to {language, topic, stars, archived}.

Dropping --json inverts the field-name bug class rather than retiring it: --json fullNam was a hard error, but jq .full_nam is null, silently. So a candidate row with a null full_name is now a hard abort, not a skipped row — a partial candidate set otherwise reads as a clean scan.

Shortlist contract

repo-shortlist.json was the only shared-state file with no schema, and its row shape is changing here (topics[]). Added schemas/repo_shortlist.schema.json + test, and validation before the atomic rename.

One wrinkle worth calling out for review: validate_json degrades to a top-level required-key check when python jsonschema is absent — which it is on a stock macOS runner. Every existing schema test gets away with this because their bad fixtures violate a top-level key. Here the keys that matter (repo, scores.final) are nested inside repos[], so the fallback would wave through the exact bug the schema exists to catch. The agent therefore asserts the row invariant separately with jq. The schema is still correct under a real validator (verified against jsonschema; rejects a row missing repo, missing scores.final, or a repo that is not owner/repo-shaped).

Render bug: commands/repo-finder.md printed \(.score), but the agent writes the score to scores.final. The score column has been null on every row. Fixed.

Verification

Ran the five queries live:

returned= 50 total_count=  100  topic:machine-learning        stars:>20000 archived:false
returned= 50 total_count=  373  topic:llm                     stars:>5000  archived:false
returned= 37 total_count=   37  topic:artificial-intelligence stars:>20000 archived:false
returned= 44 total_count=   44  topic:developer-tools         stars:>20000 archived:false
returned= 50 total_count=  484  language:java language:python stars:>20000 archived:false

pre-dedup: 231   unique: 197
  • 5 requests, down from 9. 197 unique repos after dedup (agent expects 50-100).
  • Every candidate carries all nine projected fields; zero nulls.
  • Union query confirms OR semantics: {"Java": 5, "Python": 45} on one page.
  • Static audit: every query carries a language: or topic:, no query has two topic:, every qualifier is in the allowlist, no boolean operators.
  • grep confirms the per-repo field fetch is gone and the merged-PR check survives.
  • Suite green under stock bash 3.2 / jq-1.7.1-apple. (test_audit_impact, test_parse_workflows, test_smoke_gate fail identically on untouched main — missing yq/timeout locally.)

Note for the reviewer

The write-time guard treats a zero-row shortlist as fatal, on the reading that an empty scan is far likelier a rate-limited or malformed search than a true "nothing worth contributing to" — 197 candidates surface before scoring. If you would rather a genuinely empty result write repos: [], that is a one-line relaxation.

This does not introduce the strict language AND topic filter. Default behavior stays a union of unconstrained axes, minus the three queries that matched nothing. A follow-up makes the strict filter opt-in via user preferences.

Summary by CodeRabbit

  • New Features
    • Added JSON Schema validation for repository shortlist outputs, defining required fields and structure.
    • Updated shortlist payload documentation/examples to use the repo key and include per-repository topics.
  • Bug Fixes
    • Repo-finder shortlist rendering now uses the displayed score from scores.final.
    • Hardened repository discovery/filtering and improved shortlist output persistence to prevent malformed/empty cached results.
  • Tests
    • Expanded integration tests to validate schema compliance, required row invariants, and repository discovery/rendering behavior.

…date

Step 1 searched via `gh search repos` across nine queries, then Step 2 re-fetched
pushed_at, open_issues_count, archived, and default_branch with one
`gh api repos/OWNER/REPO` per candidate, against 50-100 candidates per scan. All
four fields already ship inline in every search result.

- Search via `gh api search/repositories`. The porcelain cannot return topics[],
  which the shortlist now carries; the REST projection returns it plus the four
  fields Step 2 was re-fetching.
- Drop the per-candidate repo call. Step 2 reads the Step 1 record.
- Nine queries -> five. Repeated `language:` unions (java|python collapse to one
  request); repeated `topic:` intersects, so at most one topic per query.
- Delete the two free-text queries and the bare-stars catch-all: they carried
  neither a language nor a topic and admitted repos matching no criterion.
- `archived:false` server-side; sort by stars, not updated, which reordered
  hourly and left the per-query cutoff non-deterministic.

Live check: 5 requests, 197 unique repos after dedup, every candidate carrying
all nine projected fields.

Also pin the shortlist contract, which had no schema:
- Add schemas/repo_shortlist.schema.json + test.
- Validate before the atomic rename. validate_json degrades to a top-level
  required-key check without python jsonschema, so repo/scores.final are asserted
  separately with jq.
- Fix commands/repo-finder.md rendering `.score`, a key the agent never writes.
  The score column printed null on every row.
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f7b9f98d-01d1-4484-ab41-4cad53b1f5cc

📥 Commits

Reviewing files that changed from the base of the PR and between 7f103ee and 0b05f34.

📒 Files selected for processing (1)
  • tests/scripts/test_repo_shortlist_schema.sh

📝 Walkthrough

Walkthrough

Repository discovery now projects GitHub API fields once and reuses them for filtering. Shortlist persistence validates JSON, schema structure, and required row fields before atomic replacement. Rendering reads scores.final, with shell tests covering these contracts and control-flow guards.

Changes

Repository shortlist

Layer / File(s) Summary
Candidate discovery and filtering
agents/repo-finder.md
Repository discovery uses gh api search/repositories, projected fields, deterministic sorting, archived filtering, failure guards, and local candidate data for fast filtering without metadata refetches.
Shortlist contract and persistence
schemas/repo_shortlist.schema.json, agents/repo-finder.md
A JSON Schema defines shortlist rows with repo, topics, nested scores, and issue fields; persistence validates JSON and required row fields before atomically updating the global cache.
Rendering and regression validation
commands/repo-finder.md, tests/scripts/test_repo_shortlist_schema.sh
Shortlist rendering reads scores.final, while shell tests cover valid and invalid fixtures, row invariants, score projection, and discovery control-flow requirements.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RepoFinder
  participant GitHubSearchAPI
  participant Validator
  participant GlobalCache
  participant RepoFinderCommand
  RepoFinder->>GitHubSearchAPI: search and project repository candidates
  GitHubSearchAPI-->>RepoFinder: return projected fields and total_count
  RepoFinder->>RepoFinder: filter candidates using projected fields
  RepoFinder->>Validator: validate shortlist JSON and row invariants
  Validator-->>RepoFinder: return validation result
  RepoFinder->>GlobalCache: atomically write validated shortlist
  RepoFinderCommand->>GlobalCache: read repo-shortlist.json
  RepoFinderCommand-->>RepoFinderCommand: render scores.final
Loading

Possibly related PRs

  • gaurav0107/superhuman#25: Both PRs modify agents/repo-finder.md Step 1 and Step 2 repository discovery and filtering logic.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main performance change: cutting redundant repo-finder API calls by reusing already-fetched data.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
agents/repo-finder.md (1)

60-74: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No error handling for a failing gh api call inside the loop.

Each iteration calls gh api with no exit-code check. A rate-limited or network-failed request for one query would silently yield zero rows for that query rather than aborting or being retried — which the doc itself warns about elsewhere ("a partial candidate set reads as a clean scan") but doesn't guard against here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agents/repo-finder.md` around lines 60 - 74, The repository search loop
around DEFAULT_QUERIES must check the exit status of each gh api invocation.
Make a failed request abort the scan with a nonzero status, preserving the
documented behavior that rate-limit or network failures are not treated as an
empty result set.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@agents/repo-finder.md`:
- Around line 59-83: Update the query loop around the gh api projection to abort
immediately when any candidate item has a null or absent full_name, rather than
silently emitting or skipping it; preserve the existing projected fields for
valid rows. Also retain each response’s top-level total_count separately from
.items and report matched results against that total for every query, without
changing query thresholds or widening queries to fill the list.

In `@tests/scripts/test_repo_shortlist_schema.sh`:
- Line 3: Separate the directory resolution from the export in the
CLAUDE_PLUGIN_ROOT setup: first assign the command substitution to the variable,
allowing a failed cd to propagate under set -e, then export the variable in a
subsequent command.

---

Nitpick comments:
In `@agents/repo-finder.md`:
- Around line 60-74: The repository search loop around DEFAULT_QUERIES must
check the exit status of each gh api invocation. Make a failed request abort the
scan with a nonzero status, preserving the documented behavior that rate-limit
or network failures are not treated as an empty result set.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 917480e9-9c16-478e-a950-5f9388e52fee

📥 Commits

Reviewing files that changed from the base of the PR and between 8a9a0b1 and 9098f3b.

📒 Files selected for processing (4)
  • agents/repo-finder.md
  • commands/repo-finder.md
  • schemas/repo_shortlist.schema.json
  • tests/scripts/test_repo_shortlist_schema.sh

Comment thread agents/repo-finder.md
Comment thread tests/scripts/test_repo_shortlist_schema.sh Outdated
Review caught that Step 1 documented a hard abort on null `full_name` and
matched-of-total reporting, but the query loop implemented neither: the --jq
filter drilled straight into .items[], dropping total_count, with no null check.
A guard that exists only in prose is the failure mode this agent is meant to
remove.

- Abort on a failed `gh api` search. An unchecked exit status let a rate-limited
  query contribute zero rows to a scan that then read as clean.
- Abort on null full_name. Field-name drift yields null, not an error.
- Retain total_count and report matched-of-total, reusing the same response so
  the request count per query is unchanged.
- Feed the loop by heredoc, not `echo | while`. A piped while runs in a subshell
  where `exit` terminates only the subshell, so both aborts above would have been
  swallowed and the scan would have continued. Verified: piped form exits 0 and
  runs to completion after a fatal abort; heredoc form exits 1.
- Pin all of it in the test, including that the loop is not piped and that the
  per-candidate repo call stays deleted.

Also SC2155 in the new test: assign CLAUDE_PLUGIN_ROOT, then export, so a failed
cd is not masked by export's exit status.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/scripts/test_repo_shortlist_schema.sh`:
- Around line 66-84: The regression checks in
tests/scripts/test_repo_shortlist_schema.sh must validate executable behavior
rather than matching comments or prose. Replace the literal-text abort
assertions with representative failure and null-response fixture tests, or
strengthen them with broad syntax checks plus a positive heredoc assertion;
broaden the piped-loop rejection beyond DEFAULT_QUERIES and reject any
per-candidate gh api invocation targeting repos/, including quoted or
variable-based forms.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: be075fab-e252-49b2-8d00-ab99aa5ded6e

📥 Commits

Reviewing files that changed from the base of the PR and between 9098f3b and dd9ae6b.

📒 Files selected for processing (2)
  • agents/repo-finder.md
  • tests/scripts/test_repo_shortlist_schema.sh
🚧 Files skipped from review as they are similar to previous changes (1)
  • agents/repo-finder.md

Comment thread tests/scripts/test_repo_shortlist_schema.sh Outdated
…them

Review caught that the regression checks matched literal text anywhere in the
markdown, so a comment or a line of prose satisfied them. They asserted the
documentation, not the behavior — the same doc-vs-code drift they were added to
catch.

Extract the Step 1 block and run it against a stubbed gh (no network):
- failed search must exit nonzero and report FATAL
- null full_name must exit nonzero and report FATAL
- valid response must return rows and report matched-of-total

Structural checks now run on the block with comments stripped: reject a piped
`while` (its aborts cannot exit the scan) and require the heredoc form.

The per-candidate call is matched on its resurrection signature — a repos/ call
carrying pushed_at/open_issues_count/default_branch — not on `gh api repos/`
wholesale, as review proposed: the workflow-tree, contributors, comments, and
pulls calls are legitimate and must keep working.

Each assertion verified against a negative control: all five fail when the
behavior they guard is reverted.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/scripts/test_repo_shortlist_schema.sh`:
- Around line 105-108: Update the happy-path assertions after run_step1 in the
test to first verify that the output contains the expected owner/repo candidate
row, then retain the existing “of 63 matched” total assertion. Ensure the test
fails when projected candidates are dropped even if the matched-count text is
still emitted.
- Around line 120-127: The repository-refetch check around the AGENT scan must
detect root repos/$full_name API commands regardless of projected-field
placement or multiline formatting. Normalize complete shell commands before
matching, then reject only root repository endpoints while preserving legitimate
subresource calls such as /git/trees, /pulls, contributors, and comments; update
the grep pipeline and failure condition accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f1e70501-ebca-4b57-9d8a-1af625dd839f

📥 Commits

Reviewing files that changed from the base of the PR and between dd9ae6b and 7f103ee.

📒 Files selected for processing (1)
  • tests/scripts/test_repo_shortlist_schema.sh

Comment thread tests/scripts/test_repo_shortlist_schema.sh Outdated
Comment thread tests/scripts/test_repo_shortlist_schema.sh Outdated
Review caught both, and both were real.

Happy path asserted only the matched-of-total line, which "0 of 63 matched"
satisfies — a regression dropping every projected candidate passed. Step 1
accumulates rows into $CANDIDATES without printing them, so the runner now dumps
the variable and asserts the row and default_branch survive the projection.

Refetch check matched field names on the same line as a repos/ call, so
`gh api "repos/$full_name"` evaded it (root endpoint returns all four fields, no
field name on the line) and a line-continued command hid the field list from a
line-oriented grep. Now allowlists subresources — /git/trees, /contributors,
/issues, /pulls and friends stay legal — and rejects any other repos/ path,
however spelled. Matching the root form directly does not work: the owner/repo
may be a variable and carry no literal slash.

Fixing the first hole surfaced a bug in the harness itself: command substitution
strips the trailing newline, so the appended dump fused onto the extracted
block's heredoc terminator and the heredoc never closed.

Verified against negative controls: root refetch via variable, line-continued,
literal OWNER/REPO, and -X GET forms are each caught; the four legitimate
subresource calls still pass.
@gaurav0107 gaurav0107 merged commit d0ffcc7 into gaurav0107:main Jul 13, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants