Skip to content

feat: GitHub board (1/2): /counts data route returning issue and PR totals - #68

Merged
mabry1985 merged 1 commit into
mainfrom
feat/bd-knlyj-github-board-1-2-counts-data-route-retur
Nov 9, 2026
Merged

mabry1985 merged 1 commit into
mainfrom
feat/bd-knlyj-github-board-1-2-counts-data-route-retur

Conversation

@mabry1985

Copy link
Copy Markdown
Member

Summary

Added the gated /counts route using one GraphQL request with separate issue and PR search variables. Validates filters and response shapes, returns readable errors, and reports zero issues for merged state.

Documented the endpoint and added host-free tests. Ruff 0.15.10 lint and formatting checks pass; all 582 tests pass using installed tooling because this checkout lacks .venv.

@mabry1985
mabry1985 marked this pull request as ready for review November 9, 2026 06:17

@protoreview protoreview 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.

QA panel review — WARN

code-review-structural · head 0ac599a3abcb · formal

Low-risk PR adding a /counts endpoint, tests, and docs. No blockers or majors. Fix first: the /detail/pr route's number: int annotation breaks the {error} contract shared by all sibling detail routes. Verification refuted 5 of 10 findings — all were test-coverage claims that the verifier disproved by locating existing tests in test_counts_route.py, test_board_view.py, and test_detail_routes.py. No panel disagreement; no gaps.

Findings

Severity Location Finding Verified
🟡 minor api.py:306 GET /detail/pr declares number: int so FastAPI rejects non-numeric values with a 422 HTTP response, breaking the {error} contract that all four sibling detai… confirmed
🟡 minor api.py:159 For state=closed the PR total includes merged PRs (GitHub search is:closed includes merged), but the board's PR list via gh pr list --state closed returns … confirmed
⚪ nit api.py:262 Blocking filesystem call gh_available() runs on the event loop, violating the module's stated design principle that all blocking work runs in a worker thread. confirmed
⚪ nit api.py:361 Labels field accepts non-list values: a string like "bug" is iterated character-by-character, silently producing three single-character labels instead of one. confirmed
⚪ nit api.py:318 Unreachable except ValueError clause in _detail_number misleads readers into thinking int() can fail after the isascii/isdecimal guard. confirmed
findings JSON (machine-readable)
[
  {
    "file": "api.py",
    "line": 306,
    "severity": "minor",
    "category": "correctness",
    "claim": "GET /detail/pr declares `number: int` so FastAPI rejects non-numeric values with a 422 HTTP response, breaking the {error} contract that all four sibling detail routes uphold via `number: str` + `_detail_number()`.",
    "evidence": "All four sibling detail routes (_detail_checks, _detail_diff, _detail_conversation, _detail_issue) declare `number: str` and pass it through `_detail_number()` so that a malformed value (e.g. ?number=abc) reaches the underlying fetch function and returns the API's `{\"error\": ...}` dict. _detail_pr instead declares `number: int`, so FastAPI's type validation rejects ?number=abc with a 422 HTTP resp",
    "source": "protopatch",
    "verdict": "confirmed",
    "note": "api.py:306 `async def _detail_pr(repo: str, number: int, fresh: int = 0)` vs api.py:319/327/335/343 all use `number: str` + `_detail_number()`. FastAPI will 422 on ?number=abc for /detail/pr but return {\"error\":...} for the other four."
  },
  {
    "file": "api.py",
    "line": 159,
    "severity": "minor",
    "category": "correctness",
    "claim": "For state=closed the PR total includes merged PRs (GitHub search `is:closed` includes merged), but the board's PR list via `gh pr list --state closed` returns only closed-unmerged PRs, so the closed-tab total will exceed the rows the board can list.",
    "evidence": "# is:closed for PRs includes merged, matching gh pr list --state closed.\n    qualifier = \"\" if norm == \"all\" else f\"is:{norm}\"",
    "source": "protopatch",
    "verdict": "confirmed",
    "note": "api.py:158 uses `is:closed` in the GraphQL search for PRs. GitHub's `is:closed` for PRs includes merged PRs (well-documented search behavior), but `gh pr list --state closed` (used by fetch_prs at api.py:131) returns only closed-unmerged PRs. The code comment 'matching gh pr list --state closed' is incorrect \u2014 they do NOT match.",
    "line_original": 158,
    "line_corrected": true
  },
  {
    "file": "api.py",
    "line": 262,
    "severity": "nit",
    "category": "conventions",
    "claim": "Blocking filesystem call gh_available() runs on the event loop, violating the module's stated design principle that all blocking work runs in a worker thread.",
    "evidence": "The module docstring (lines 262\u2013263) states the design principle: 'The picker/default resolution may parse git remotes (blocking, cached) \u2014 every route runs it in a worker thread, never on the event loop.' The `_resolved()` helper correctly offloads `resolve_config` via `asyncio.to_thread`, but line 281 calls `gh_available()` directly on the event loop.",
    "source": "protopatch",
    "verdict": "confirmed",
    "note": "api.py:281 `return {**resolved, \"gh_available\": gh_available()}` calls gh_available() (which calls resolve_gh() \u2192 filesystem PATH/install-dir checks) directly in the async _config handler. The docstring at api.py:262-263 explicitly states 'every route runs it in a worker thread, never on the event loop.'"
  },
  {
    "file": "api.py",
    "line": 361,
    "severity": "nit",
    "category": "correctness",
    "claim": "Labels field accepts non-list values: a string like \"bug\" is iterated character-by-character, silently producing three single-character labels instead of one.",
    "evidence": "`[str(x) for x in (body.get(\"labels\") or [])]` iterates over whatever value is in `body[\"labels\"]`. If a client sends a string like `{\"labels\": \"bug\"}` instead of `{\"labels\": [\"bug\"]}`, the comprehension yields `['b','u','g']` \u2014 three single-character labels \u2014 rather than one label.",
    "source": "protopatch",
    "verdict": "confirmed",
    "note": "api.py:361 `labels = labels_for(kind, [str(x) for x in (body.get(\"labels\") or [])])` \u2014 if body[\"labels\"] is the string \"bug\", the for-loop iterates characters yielding ['b','u','g']. No type check on the value before iteration."
  },
  {
    "file": "api.py",
    "line": 318,
    "severity": "nit",
    "category": "conventions",
    "claim": "Unreachable except ValueError clause in _detail_number misleads readers into thinking int() can fail after the isascii/isdecimal guard.",
    "evidence": "The guard `value.isascii() and value.isdecimal()` ensures `value` contains only ASCII characters '0'-'9' (isdecimal() is False for '', '+', '-', underscores, and non-ASCII digit characters; isascii() excludes non-ASCII). `int()` on a non-empty string of ASCII digits never raises ValueError. The `try/except ValueError` block is therefore dead code.",
    "source": "protopatch",
    "verdict": "confirmed",
    "note": "api.py:315-318: `return int(value) if value.isascii() and value.isdecimal() else value` \u2014 isdecimal() is True only for '0'-'9' (and some Unicode digits, but isascii() excludes those). int() on a non-empty string of '0'-'9' never raises ValueError. The except clause is unreachable.",
    "line_original": 315,
    "line_corrected": true
  }
]

@protoreview protoreview 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.

Promoting the WARN verdict for head 0ac599a3abcb: all checks terminal-green, zero unresolved review threads. (approve-on-green)

Open findings carried by this approval — non-blocking, but they did not go away:

  • minor api.py:306 — GET /detail/pr declares number: int so FastAPI rejects non-numeric values with a 422 HTTP response, breaking the {error} contract that all four sibling detail routes uphold via number: str + `_det
  • minor api.py:159 — For state=closed the PR total includes merged PRs (GitHub search is:closed includes merged), but the board's PR list via gh pr list --state closed returns only closed-unmerged PRs, so the closed-t
  • nit api.py:262 — Blocking filesystem call gh_available() runs on the event loop, violating the module's stated design principle that all blocking work runs in a worker thread.
  • nit api.py:361 — Labels field accepts non-list values: a string like "bug" is iterated character-by-character, silently producing three single-character labels instead of one.
  • nit api.py:318 — Unreachable except ValueError clause in _detail_number misleads readers into thinking int() can fail after the isascii/isdecimal guard.

Approving a WARN does not resolve its findings (issue #22).

@mabry1985
mabry1985 merged commit 02da781 into main Nov 9, 2026
4 checks passed
@mabry1985
mabry1985 deleted the feat/bd-knlyj-github-board-1-2-counts-data-route-retur branch November 9, 2026 06:32
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.

1 participant