ghtriage 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ghtriage-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jay Qi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,220 @@
1
+ Metadata-Version: 2.4
2
+ Name: ghtriage
3
+ Version: 0.1.0
4
+ Summary: A local, queryable snapshot of a GitHub repository's issues, pull requests, and comments. Built for AI coding agents to assist with project management.
5
+ Keywords: github,issues,pull requests,project management
6
+ Author: Jay Qi
7
+ Author-email: Jay Qi <jayqi.opensource@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Topic :: Software Development :: Bug Tracking
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Requires-Dist: dlt[duckdb]>=1.0,<2
20
+ Requires-Dist: duckdb>=1.2
21
+ Requires-Dist: tomli>=2.0.1 ; python_full_version < '3.11'
22
+ Requires-Dist: tomlkit>=0.14.0
23
+ Maintainer: Jay Qi, DrivenData
24
+ Maintainer-email: Jay Qi <jayqi.opensource@gmail.com>, DrivenData <info@drivendata.org>
25
+ Requires-Python: >=3.10
26
+ Project-URL: Repository, https://github.com/drivendataorg/ghtriage
27
+ Project-URL: Bug Tracker, https://github.com/drivendataorg/ghtriage/issues
28
+ Description-Content-Type: text/markdown
29
+
30
+ # ghtriage
31
+
32
+ **A local, queryable snapshot of a GitHub repository's issues, pull requests, and comments. Built for AI coding agents to assist with project management.**
33
+
34
+ **ghtriage** pulls a repository's GitHub data into a local DuckDB database that an agent can cheaply and quickly query with SQL. The motivating use case is project management and triage: an agent asked to find stale issues, likely duplicates, or unanswered questions can answer from the local database in milliseconds instead of paging through the GitHub API, complemented by the commit history already available in the local Git repository.
35
+
36
+ The command-line interface (CLI) provides commands for:
37
+
38
+ - setting up GitHub authentication and showing which token source is in use
39
+ - pulling all issue, pull request, and comment data for a GitHub repository into the local database
40
+ - showing the state and freshness of the local database
41
+ - inspecting the database schema, including column documentation
42
+ - querying the database with SQL
43
+
44
+ ghtriage deliberately provides data and access, not judgments. It pre-computes facts that are awkward to derive from the raw tables, but it never scores, ranks, or decides what "stale" or "needs attention" means—that's the user's job to define within the context of their project.
45
+
46
+ ## Installation
47
+
48
+ Install ghtriage as a CLI tool with [uv](https://docs.astral.sh/uv/):
49
+
50
+ ```bash
51
+ uv tool install ghtriage
52
+ ```
53
+
54
+ Or run it without installing:
55
+
56
+ ```bash
57
+ uvx ghtriage --help
58
+ ```
59
+
60
+ Any tool that installs from PyPI (`pip`, `pipx`) also works. Requires Python 3.10+.
61
+
62
+ ## Agent skill
63
+
64
+ ghtriage ships an [Agent Skill](https://agentskills.io/specification) — a `SKILL.md` plus a query cookbook — that teaches a coding agent when to reach for ghtriage and how to query the database. There are two ways to install it:
65
+
66
+ ```bash
67
+ ghtriage skill install # [recommended] from the CLI you have installed
68
+ gh skill install drivendataorg/ghtriage ghtriage # from the repository (pin a ref with --pin)
69
+ ```
70
+
71
+ Installing using `ghtriage` will install a compatible version that is bundled with the package. Re-run the command after upgrading ghtriage to upgrade in place. A skill directory without a `managed-by: ghtriage` marker is left alone unless you pass `--force`.
72
+
73
+ `--agent` says whose skills directory to target: `claude-code` for Claude Code (`.claude/skills/`), or `universal` for the shared `.agents/skills/` directory that Copilot, Cursor, Codex, Gemini CLI, and others read. It has no default; omit it and the command asks. `--scope` defaults to `project`, which installs into the current directory. `--scope user` installs into your home directory instead, and `--dir PATH` installs to `PATH/ghtriage/` for anything else.
74
+
75
+ ## Usage
76
+
77
+ ### Setup
78
+
79
+ ghtriage needs a GitHub token to pull data. Run:
80
+
81
+ ```bash
82
+ ghtriage auth setup
83
+ ```
84
+
85
+ It offers three methods and makes no network calls of its own:
86
+
87
+ 1. **Fine-grained personal access token** (recommended). Prints a prefilled link that asks for read-only access to issues and pull requests. You pick the resource owner and the single repository on GitHub's page; the token is saved to `.ghtriage/token` with `0600` permissions (on POSIX systems; Windows file access is governed by ACLs instead).
88
+ 2. **Classic personal access token**. Prints a prefilled link for the `repo` scope. That scope grants read *and* write to everything you can reach — use it when your organization blocks fine-grained tokens or approval of one is stuck.
89
+ 3. **Reuse your gh CLI login**. Sets `[auth] use_gh_token = true` in `.ghtriage/config.toml`, after which ghtriage falls back to `gh auth token`. Convenient, but that token is much broader than a fine-grained PAT. `ghtriage auth setup --use-gh-token` picks this without the menu.
90
+
91
+ Tokens are resolved in the following order, and `ghtriage auth status` shows every source and which one wins:
92
+
93
+ ```
94
+ GITHUB_TOKEN (env) not set
95
+ .ghtriage/token found <- in use
96
+ gh auth token disabled (enable: ghtriage auth setup --use-gh-token)
97
+ ```
98
+
99
+ Scripts and agents can skip the command entirely: set `GITHUB_TOKEN`, or write the token to `.ghtriage/token` directly.
100
+
101
+ By default, `pull` targets the repository of the current directory's git `origin` remote. To target a different repository, use the `--repo` flag, or set it in `.ghtriage/config.toml`:
102
+
103
+ ```toml
104
+ repo = "OWNER/REPO"
105
+ ```
106
+
107
+ `.ghtriage/config.toml` is scaffolded when the `.ghtriage` directory is created with every key commented out.
108
+
109
+ ### Commands
110
+
111
+ ```bash
112
+ ghtriage auth setup [--use-gh-token]
113
+ ghtriage auth status
114
+ ghtriage pull [--repo OWNER/REPO] [--full]
115
+ ghtriage status
116
+ ghtriage schema [--table TABLE_NAME]
117
+ ghtriage query "SQL statement" [--format table|csv|json]
118
+ ghtriage skill install [--agent claude-code|universal] [--scope project|user] [--dir PATH] [--force]
119
+ ```
120
+
121
+ ### Query formats
122
+
123
+ - `table`: column-aligned text output with full values.
124
+ - `csv`: header row followed by CSV rows.
125
+ - `json`: strict JSONL (one JSON object per row).
126
+
127
+ ### Examples
128
+
129
+ ```bash
130
+ ghtriage schema
131
+ ghtriage schema --table issues
132
+ ghtriage schema --table issue_activity
133
+ ghtriage query "SELECT number, title, state FROM issues LIMIT 5"
134
+ ghtriage query "SELECT count(*) AS n FROM issues" --format json
135
+ ghtriage query "SELECT number, title FROM issue_activity WHERE state = 'open' AND first_non_author_comment_at IS NULL"
136
+ ghtriage query "SELECT number, title, score FROM (SELECT number, title, round(fts_github_issue_threads.match_bm25(id, 'cache invalidation'), 2) AS score FROM issue_activity) WHERE score IS NOT NULL ORDER BY score DESC LIMIT 5"
137
+ ```
138
+
139
+ ### Exit codes
140
+
141
+ - `0`: command completed successfully.
142
+ - `1`: runtime failure (for example missing database, SQL error, unknown table).
143
+ - `2`: command usage/argument error (argparse-level failure).
144
+
145
+ ## How it works
146
+
147
+ `pull` fetches data from the GitHub REST API and writes it to a DuckDB database in a `.ghtriage/` directory in the current working directory:
148
+
149
+ ```
150
+ .ghtriage/
151
+ ├── config.toml # configuration: target repository, auth preference
152
+ ├── token # GitHub token, if not using the GITHUB_TOKEN env var
153
+ ├── ghtriage.duckdb # the DuckDB database
154
+ └── pipelines/ # incremental pull state
155
+ ```
156
+
157
+ The directory manages its own `.gitignore` and excludes all of itself from version control. The whole directory is a disposable cache: delete it and `ghtriage auth setup` plus `ghtriage pull` rebuild it.
158
+
159
+ Some behaviors to be aware of:
160
+
161
+ - **The database is a snapshot.** It reflects GitHub as of the last `pull` and never updates on its own. Use `status` to see what repository is in the database and how fresh the data is.
162
+ - **Pulls are incremental.** Re-running `pull` fetches only what changed since the last pull, so it is cheap to run often. Use `--full` to delete the database and rebuild from scratch. After upgrading to a ghtriage version that changes the database layout, `pull` refuses and asks for a one-time `--full`.
163
+ - **The target repository is resolved automatically.** In order of precedence: the `--repo` flag, the `repo` key in `.ghtriage/config.toml`, then the current repository's git `origin` remote.
164
+
165
+ ### What gets pulled
166
+
167
+ | Table | Contents |
168
+ |---|---|
169
+ | `issues` | Issues only. Pull requests are filtered out, though GitHub's endpoint returns both. |
170
+ | `pull_requests` | Pull requests. |
171
+ | `conversation_comments` | Comments on the main thread of the issue or pull request. |
172
+ | `review_comments` | Inline comments on a pull request's diff. |
173
+
174
+ Nested arrays become child tables named with a double-underscore, e.g., `issues__labels`, and each carries its parent's GitHub key — join `issues__labels.issue_number = issues.number`. Run `ghtriage schema --table <name>` to see the key any child table carries and what it joins to.
175
+
176
+ If any entity has zero records, no table is created rather than an empty one. Run `ghtriage schema` for the authoritative list of what your
177
+ database actually holds.
178
+
179
+ ### Derived tables and views
180
+
181
+ Every `ghtriage pull` also builds four derived objects. All are rebuilt each time the data refreshes, and every column carries a description you can read with `ghtriage schema --table <name>`.
182
+
183
+ - **`issue_activity`** — one row per issue, with comment counts and timestamps, labels, and assignees already joined.
184
+ - **`pull_request_activity`** — one row per pull request, the same plus review-comment facts and pending review requests.
185
+ - **`issue_threads`** — one full-text document per issue: its title, body, and every conversation comment on it, oldest first.
186
+ - **`pull_request_threads`** — one full-text document per pull request, folding in both conversation and review comments.
187
+
188
+ The thread tables exist to be searched — see [Full-text search](#full-text-search).
189
+
190
+ Details worth knowing:
191
+
192
+ - **A repository with zero issues or zero pull requests will not get the respective view or thread table.** Each is built from a base table, and there is no table until at least one record of that kind has been pulled. Query `ghtriage schema` to see what exists rather than assuming.
193
+ - **Pull requests have two separate comment channels.** GitHub's issue-comments endpoint carries conversation comments on both issues and pull requests; the pull-comments endpoint carries only inline review comments. `pull_request_activity` keeps them as separate columns — a PR can have a long discussion and no code review, or the reverse — while `pull_request_threads` folds them together.
194
+ - **Bot activity is split out, not filtered.** `comment_count` counts everything; `non_bot_comment_count` counts only accounts GitHub does not type as `Bot`. Subtract for the bot count. The same pattern applies to review comments and participants.
195
+
196
+ ### Full-text search
197
+
198
+ Every pull also builds BM25 full-text indexes, so "has this been reported before?" is a ranked query rather than a scan. The six indexes cover three different corpora over the same entities — pick the index by the question you are asking, not just by the table name:
199
+
200
+ | Indexes | One document per | Search it to answer |
201
+ |---|---|---|
202
+ | `fts_github_issues` / `fts_github_pull_requests` | issue or pull request — **title and body only** | "Is there an issue *about* this?" — the author's own description, without noise from whatever was later said in the comments |
203
+ | `fts_github_issue_threads` / `fts_github_pull_request_threads` | issue or pull request — **title, body, and every comment** | "Has this been *discussed* anywhere?" — a mention deep in a long thread still matches |
204
+ | `fts_github_conversation_comments` / `fts_github_review_comments` | **single comment** — its body | "Which comment said it?" — each comment is scored on its own, so a thread that circles a topic ranks below one comment that says it all |
205
+
206
+ Each index lives in a schema named for its table — `fts_github_issues`, `fts_github_issue_threads` — and exposes `match_bm25(id, 'query')`:
207
+
208
+ ```bash
209
+ ghtriage query "SELECT number, title, score FROM (SELECT number, title, round(fts_github_issue_threads.match_bm25(id, 'timeout uploading large files'), 2) AS score FROM issue_activity) WHERE score IS NOT NULL ORDER BY score DESC LIMIT 10"
210
+ ```
211
+
212
+ Run `ghtriage schema` for the indexes your database actually holds, with their columns and document counts. Things worth knowing:
213
+
214
+ - **The score function is keyed on the document id, not bound to its table.** Both derived views carry `id`, so one relation gives you search and derived facts together — the example above searches whole threads while selecting from `issue_activity`. Filter on both at once: `SELECT number, title FROM issue_activity WHERE fts_github_issues.match_bm25(id, 'windows path') IS NOT NULL AND state = 'open'`.
215
+ - **A table and its thread table share ids — their indexes differ in text, not in entities.** `fts_github_issues.match_bm25(id, …)` and `fts_github_issue_threads.match_bm25(id, …)` accept the same ids and score the same issue against different corpora, so mixing them up is not an error — it answers the other question in the table above. An id an index does not hold (a pull request id against an issue index) scores `NULL`.
216
+ - **Digits are not indexed.** The tokenizer strips them, so searching `404` finds nothing. Use `LIKE` or `regexp_matches` for exact codes, versions, and identifiers.
217
+ - **Terms are OR-ed and stemmed by default.** `'azure credential'` matches documents containing either word, and `renaming` matches `rename`. Pass `conjunctive := 1` to require every term.
218
+ - **Every word is indexed — there is no stopword list.** On software text, `use`, `get`, and `old` are vocabulary, not noise, and BM25's frequency weighting already keeps ubiquitous words from dominating a ranking. The flip side: a query containing a very common word matches nearly every document, so read the ranking, not the match count.
219
+ - **Scores rank results within a single query; they are not a similarity measure.** A score's scale depends on the corpus and the query's terms, so scores are not comparable across queries or across indexes. Take the top `n` with `ORDER BY score DESC`; don't filter on a fixed threshold.
220
+ - **Indexes are rebuilt from scratch on every pull.** On a repository with ~2,500 documents this adds about half a second to a pull and roughly 40% to the database file.
@@ -0,0 +1,191 @@
1
+ # ghtriage
2
+
3
+ **A local, queryable snapshot of a GitHub repository's issues, pull requests, and comments. Built for AI coding agents to assist with project management.**
4
+
5
+ **ghtriage** pulls a repository's GitHub data into a local DuckDB database that an agent can cheaply and quickly query with SQL. The motivating use case is project management and triage: an agent asked to find stale issues, likely duplicates, or unanswered questions can answer from the local database in milliseconds instead of paging through the GitHub API, complemented by the commit history already available in the local Git repository.
6
+
7
+ The command-line interface (CLI) provides commands for:
8
+
9
+ - setting up GitHub authentication and showing which token source is in use
10
+ - pulling all issue, pull request, and comment data for a GitHub repository into the local database
11
+ - showing the state and freshness of the local database
12
+ - inspecting the database schema, including column documentation
13
+ - querying the database with SQL
14
+
15
+ ghtriage deliberately provides data and access, not judgments. It pre-computes facts that are awkward to derive from the raw tables, but it never scores, ranks, or decides what "stale" or "needs attention" means—that's the user's job to define within the context of their project.
16
+
17
+ ## Installation
18
+
19
+ Install ghtriage as a CLI tool with [uv](https://docs.astral.sh/uv/):
20
+
21
+ ```bash
22
+ uv tool install ghtriage
23
+ ```
24
+
25
+ Or run it without installing:
26
+
27
+ ```bash
28
+ uvx ghtriage --help
29
+ ```
30
+
31
+ Any tool that installs from PyPI (`pip`, `pipx`) also works. Requires Python 3.10+.
32
+
33
+ ## Agent skill
34
+
35
+ ghtriage ships an [Agent Skill](https://agentskills.io/specification) — a `SKILL.md` plus a query cookbook — that teaches a coding agent when to reach for ghtriage and how to query the database. There are two ways to install it:
36
+
37
+ ```bash
38
+ ghtriage skill install # [recommended] from the CLI you have installed
39
+ gh skill install drivendataorg/ghtriage ghtriage # from the repository (pin a ref with --pin)
40
+ ```
41
+
42
+ Installing using `ghtriage` will install a compatible version that is bundled with the package. Re-run the command after upgrading ghtriage to upgrade in place. A skill directory without a `managed-by: ghtriage` marker is left alone unless you pass `--force`.
43
+
44
+ `--agent` says whose skills directory to target: `claude-code` for Claude Code (`.claude/skills/`), or `universal` for the shared `.agents/skills/` directory that Copilot, Cursor, Codex, Gemini CLI, and others read. It has no default; omit it and the command asks. `--scope` defaults to `project`, which installs into the current directory. `--scope user` installs into your home directory instead, and `--dir PATH` installs to `PATH/ghtriage/` for anything else.
45
+
46
+ ## Usage
47
+
48
+ ### Setup
49
+
50
+ ghtriage needs a GitHub token to pull data. Run:
51
+
52
+ ```bash
53
+ ghtriage auth setup
54
+ ```
55
+
56
+ It offers three methods and makes no network calls of its own:
57
+
58
+ 1. **Fine-grained personal access token** (recommended). Prints a prefilled link that asks for read-only access to issues and pull requests. You pick the resource owner and the single repository on GitHub's page; the token is saved to `.ghtriage/token` with `0600` permissions (on POSIX systems; Windows file access is governed by ACLs instead).
59
+ 2. **Classic personal access token**. Prints a prefilled link for the `repo` scope. That scope grants read *and* write to everything you can reach — use it when your organization blocks fine-grained tokens or approval of one is stuck.
60
+ 3. **Reuse your gh CLI login**. Sets `[auth] use_gh_token = true` in `.ghtriage/config.toml`, after which ghtriage falls back to `gh auth token`. Convenient, but that token is much broader than a fine-grained PAT. `ghtriage auth setup --use-gh-token` picks this without the menu.
61
+
62
+ Tokens are resolved in the following order, and `ghtriage auth status` shows every source and which one wins:
63
+
64
+ ```
65
+ GITHUB_TOKEN (env) not set
66
+ .ghtriage/token found <- in use
67
+ gh auth token disabled (enable: ghtriage auth setup --use-gh-token)
68
+ ```
69
+
70
+ Scripts and agents can skip the command entirely: set `GITHUB_TOKEN`, or write the token to `.ghtriage/token` directly.
71
+
72
+ By default, `pull` targets the repository of the current directory's git `origin` remote. To target a different repository, use the `--repo` flag, or set it in `.ghtriage/config.toml`:
73
+
74
+ ```toml
75
+ repo = "OWNER/REPO"
76
+ ```
77
+
78
+ `.ghtriage/config.toml` is scaffolded when the `.ghtriage` directory is created with every key commented out.
79
+
80
+ ### Commands
81
+
82
+ ```bash
83
+ ghtriage auth setup [--use-gh-token]
84
+ ghtriage auth status
85
+ ghtriage pull [--repo OWNER/REPO] [--full]
86
+ ghtriage status
87
+ ghtriage schema [--table TABLE_NAME]
88
+ ghtriage query "SQL statement" [--format table|csv|json]
89
+ ghtriage skill install [--agent claude-code|universal] [--scope project|user] [--dir PATH] [--force]
90
+ ```
91
+
92
+ ### Query formats
93
+
94
+ - `table`: column-aligned text output with full values.
95
+ - `csv`: header row followed by CSV rows.
96
+ - `json`: strict JSONL (one JSON object per row).
97
+
98
+ ### Examples
99
+
100
+ ```bash
101
+ ghtriage schema
102
+ ghtriage schema --table issues
103
+ ghtriage schema --table issue_activity
104
+ ghtriage query "SELECT number, title, state FROM issues LIMIT 5"
105
+ ghtriage query "SELECT count(*) AS n FROM issues" --format json
106
+ ghtriage query "SELECT number, title FROM issue_activity WHERE state = 'open' AND first_non_author_comment_at IS NULL"
107
+ ghtriage query "SELECT number, title, score FROM (SELECT number, title, round(fts_github_issue_threads.match_bm25(id, 'cache invalidation'), 2) AS score FROM issue_activity) WHERE score IS NOT NULL ORDER BY score DESC LIMIT 5"
108
+ ```
109
+
110
+ ### Exit codes
111
+
112
+ - `0`: command completed successfully.
113
+ - `1`: runtime failure (for example missing database, SQL error, unknown table).
114
+ - `2`: command usage/argument error (argparse-level failure).
115
+
116
+ ## How it works
117
+
118
+ `pull` fetches data from the GitHub REST API and writes it to a DuckDB database in a `.ghtriage/` directory in the current working directory:
119
+
120
+ ```
121
+ .ghtriage/
122
+ ├── config.toml # configuration: target repository, auth preference
123
+ ├── token # GitHub token, if not using the GITHUB_TOKEN env var
124
+ ├── ghtriage.duckdb # the DuckDB database
125
+ └── pipelines/ # incremental pull state
126
+ ```
127
+
128
+ The directory manages its own `.gitignore` and excludes all of itself from version control. The whole directory is a disposable cache: delete it and `ghtriage auth setup` plus `ghtriage pull` rebuild it.
129
+
130
+ Some behaviors to be aware of:
131
+
132
+ - **The database is a snapshot.** It reflects GitHub as of the last `pull` and never updates on its own. Use `status` to see what repository is in the database and how fresh the data is.
133
+ - **Pulls are incremental.** Re-running `pull` fetches only what changed since the last pull, so it is cheap to run often. Use `--full` to delete the database and rebuild from scratch. After upgrading to a ghtriage version that changes the database layout, `pull` refuses and asks for a one-time `--full`.
134
+ - **The target repository is resolved automatically.** In order of precedence: the `--repo` flag, the `repo` key in `.ghtriage/config.toml`, then the current repository's git `origin` remote.
135
+
136
+ ### What gets pulled
137
+
138
+ | Table | Contents |
139
+ |---|---|
140
+ | `issues` | Issues only. Pull requests are filtered out, though GitHub's endpoint returns both. |
141
+ | `pull_requests` | Pull requests. |
142
+ | `conversation_comments` | Comments on the main thread of the issue or pull request. |
143
+ | `review_comments` | Inline comments on a pull request's diff. |
144
+
145
+ Nested arrays become child tables named with a double-underscore, e.g., `issues__labels`, and each carries its parent's GitHub key — join `issues__labels.issue_number = issues.number`. Run `ghtriage schema --table <name>` to see the key any child table carries and what it joins to.
146
+
147
+ If any entity has zero records, no table is created rather than an empty one. Run `ghtriage schema` for the authoritative list of what your
148
+ database actually holds.
149
+
150
+ ### Derived tables and views
151
+
152
+ Every `ghtriage pull` also builds four derived objects. All are rebuilt each time the data refreshes, and every column carries a description you can read with `ghtriage schema --table <name>`.
153
+
154
+ - **`issue_activity`** — one row per issue, with comment counts and timestamps, labels, and assignees already joined.
155
+ - **`pull_request_activity`** — one row per pull request, the same plus review-comment facts and pending review requests.
156
+ - **`issue_threads`** — one full-text document per issue: its title, body, and every conversation comment on it, oldest first.
157
+ - **`pull_request_threads`** — one full-text document per pull request, folding in both conversation and review comments.
158
+
159
+ The thread tables exist to be searched — see [Full-text search](#full-text-search).
160
+
161
+ Details worth knowing:
162
+
163
+ - **A repository with zero issues or zero pull requests will not get the respective view or thread table.** Each is built from a base table, and there is no table until at least one record of that kind has been pulled. Query `ghtriage schema` to see what exists rather than assuming.
164
+ - **Pull requests have two separate comment channels.** GitHub's issue-comments endpoint carries conversation comments on both issues and pull requests; the pull-comments endpoint carries only inline review comments. `pull_request_activity` keeps them as separate columns — a PR can have a long discussion and no code review, or the reverse — while `pull_request_threads` folds them together.
165
+ - **Bot activity is split out, not filtered.** `comment_count` counts everything; `non_bot_comment_count` counts only accounts GitHub does not type as `Bot`. Subtract for the bot count. The same pattern applies to review comments and participants.
166
+
167
+ ### Full-text search
168
+
169
+ Every pull also builds BM25 full-text indexes, so "has this been reported before?" is a ranked query rather than a scan. The six indexes cover three different corpora over the same entities — pick the index by the question you are asking, not just by the table name:
170
+
171
+ | Indexes | One document per | Search it to answer |
172
+ |---|---|---|
173
+ | `fts_github_issues` / `fts_github_pull_requests` | issue or pull request — **title and body only** | "Is there an issue *about* this?" — the author's own description, without noise from whatever was later said in the comments |
174
+ | `fts_github_issue_threads` / `fts_github_pull_request_threads` | issue or pull request — **title, body, and every comment** | "Has this been *discussed* anywhere?" — a mention deep in a long thread still matches |
175
+ | `fts_github_conversation_comments` / `fts_github_review_comments` | **single comment** — its body | "Which comment said it?" — each comment is scored on its own, so a thread that circles a topic ranks below one comment that says it all |
176
+
177
+ Each index lives in a schema named for its table — `fts_github_issues`, `fts_github_issue_threads` — and exposes `match_bm25(id, 'query')`:
178
+
179
+ ```bash
180
+ ghtriage query "SELECT number, title, score FROM (SELECT number, title, round(fts_github_issue_threads.match_bm25(id, 'timeout uploading large files'), 2) AS score FROM issue_activity) WHERE score IS NOT NULL ORDER BY score DESC LIMIT 10"
181
+ ```
182
+
183
+ Run `ghtriage schema` for the indexes your database actually holds, with their columns and document counts. Things worth knowing:
184
+
185
+ - **The score function is keyed on the document id, not bound to its table.** Both derived views carry `id`, so one relation gives you search and derived facts together — the example above searches whole threads while selecting from `issue_activity`. Filter on both at once: `SELECT number, title FROM issue_activity WHERE fts_github_issues.match_bm25(id, 'windows path') IS NOT NULL AND state = 'open'`.
186
+ - **A table and its thread table share ids — their indexes differ in text, not in entities.** `fts_github_issues.match_bm25(id, …)` and `fts_github_issue_threads.match_bm25(id, …)` accept the same ids and score the same issue against different corpora, so mixing them up is not an error — it answers the other question in the table above. An id an index does not hold (a pull request id against an issue index) scores `NULL`.
187
+ - **Digits are not indexed.** The tokenizer strips them, so searching `404` finds nothing. Use `LIKE` or `regexp_matches` for exact codes, versions, and identifiers.
188
+ - **Terms are OR-ed and stemmed by default.** `'azure credential'` matches documents containing either word, and `renaming` matches `rename`. Pass `conjunctive := 1` to require every term.
189
+ - **Every word is indexed — there is no stopword list.** On software text, `use`, `get`, and `old` are vocabulary, not noise, and BM25's frequency weighting already keeps ubiquitous words from dominating a ranking. The flip side: a query containing a very common word matches nearly every document, so read the ranking, not the match count.
190
+ - **Scores rank results within a single query; they are not a similarity measure.** A score's scale depends on the corpus and the query's terms, so scores are not comparable across queries or across indexes. Take the top `n` with `ORDER BY score DESC`; don't filter on a fixed threshold.
191
+ - **Indexes are rebuilt from scratch on every pull.** On a repository with ~2,500 documents this adds about half a second to a pull and roughly 40% to the database file.
@@ -0,0 +1,108 @@
1
+ [project]
2
+ name = "ghtriage"
3
+ version = "0.1.0"
4
+ description = "A local, queryable snapshot of a GitHub repository's issues, pull requests, and comments. Built for AI coding agents to assist with project management."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ keywords = [
9
+ "github",
10
+ "issues",
11
+ "pull requests",
12
+ "project management",
13
+ ]
14
+ classifiers = [
15
+ "Intended Audience :: Developers",
16
+ "Topic :: Software Development :: Bug Tracking",
17
+ "Operating System :: OS Independent",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
24
+ ]
25
+ requires-python = ">=3.10"
26
+ dependencies = [
27
+ "dlt[duckdb]>=1.0,<2",
28
+ "duckdb>=1.2",
29
+ "tomli>=2.0.1; python_version < '3.11'",
30
+ "tomlkit>=0.14.0",
31
+ ]
32
+
33
+ [[project.authors]]
34
+ name = "Jay Qi"
35
+ email = "jayqi.opensource@gmail.com"
36
+
37
+ [[project.maintainers]]
38
+ name = "Jay Qi"
39
+ email = "jayqi.opensource@gmail.com"
40
+
41
+ [[project.maintainers]]
42
+ name = "DrivenData"
43
+ email = "info@drivendata.org"
44
+
45
+ [project.urls]
46
+ Repository = "https://github.com/drivendataorg/ghtriage"
47
+ "Bug Tracker" = "https://github.com/drivendataorg/ghtriage/issues"
48
+
49
+ [project.scripts]
50
+ ghtriage = "ghtriage:main"
51
+
52
+ [build-system]
53
+ requires = ["uv_build>=0.10.2,<0.11.0"]
54
+ build-backend = "uv_build"
55
+
56
+ [dependency-groups]
57
+ dev = [
58
+ "ruff",
59
+ "zizmor",
60
+ { include-group = "tests" },
61
+ { include-group = "typecheck" },
62
+ ]
63
+ tests = [
64
+ "coverage",
65
+ "pytest-cov",
66
+ "pytest",
67
+ ]
68
+ typecheck = ["ty"]
69
+ vspect = ["vspect"]
70
+
71
+ [tool.uv]
72
+ exclude-newer = "7 days"
73
+
74
+ [tool.ruff]
75
+ line-length = 99
76
+ src = [
77
+ "src/ghtriage/**/*.py",
78
+ "tests/**/*.py",
79
+ ]
80
+
81
+ [tool.ruff.lint]
82
+ select = [
83
+ "E",
84
+ "F",
85
+ "I",
86
+ ]
87
+ unfixable = ["F"]
88
+
89
+ [tool.ruff.lint.isort]
90
+ known-first-party = ["ghtriage"]
91
+ force-sort-within-sections = true
92
+
93
+ [tool.ty.src]
94
+ include = ["src"]
95
+
96
+ [tool.pytest.ini_options]
97
+ minversion = "6.0"
98
+ addopts = "--cov=ghtriage --cov-report=term --cov-report=html --cov-report=xml"
99
+ testpaths = ["tests"]
100
+
101
+ [tool.coverage.run]
102
+ source = ["ghtriage"]
103
+
104
+ [tool.coverage.paths]
105
+ ghtriage = [
106
+ "src/ghtriage/",
107
+ "*/site-packages/ghtriage/",
108
+ ]
@@ -0,0 +1,107 @@
1
+ [project]
2
+ name = "ghtriage"
3
+ version = "0.1.0"
4
+ description = "A local, queryable snapshot of a GitHub repository's issues, pull requests, and comments. Built for AI coding agents to assist with project management."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Jay Qi", email = "jayqi.opensource@gmail.com" },
8
+ ]
9
+ maintainers = [
10
+ { name = "Jay Qi", email = "jayqi.opensource@gmail.com" },
11
+ { name = "DrivenData", email = "info@drivendata.org" },
12
+ ]
13
+ license = "MIT"
14
+ license-files = ["LICENSE"]
15
+ keywords = [
16
+ "github",
17
+ "issues",
18
+ "pull requests",
19
+ "project management",
20
+ ]
21
+ classifiers = [
22
+ "Intended Audience :: Developers",
23
+ "Topic :: Software Development :: Bug Tracking",
24
+ "Operating System :: OS Independent",
25
+ "Programming Language :: Python :: 3",
26
+ "Programming Language :: Python :: 3.10",
27
+ "Programming Language :: Python :: 3.11",
28
+ "Programming Language :: Python :: 3.12",
29
+ "Programming Language :: Python :: 3.13",
30
+ "Programming Language :: Python :: 3.14",
31
+ ]
32
+ requires-python = ">=3.10"
33
+ dependencies = [
34
+ "dlt[duckdb]>=1.0,<2",
35
+ "duckdb>=1.2",
36
+ "tomli>=2.0.1; python_version < '3.11'",
37
+ "tomlkit>=0.14.0",
38
+ ]
39
+
40
+ [project.urls]
41
+ "Repository" = "https://github.com/drivendataorg/ghtriage"
42
+ "Bug Tracker" = "https://github.com/drivendataorg/ghtriage/issues"
43
+
44
+ [project.scripts]
45
+ ghtriage = "ghtriage:main"
46
+
47
+ [build-system]
48
+ requires = ["uv_build>=0.10.2,<0.11.0"]
49
+ build-backend = "uv_build"
50
+
51
+ [dependency-groups]
52
+ dev = [
53
+ "ruff",
54
+ "zizmor",
55
+ { include-group = "tests" },
56
+ { include-group = "typecheck" },
57
+ ]
58
+ tests = [
59
+ "coverage",
60
+ "pytest-cov",
61
+ "pytest",
62
+ ]
63
+ typecheck = [
64
+ "ty",
65
+ ]
66
+ vspect = [
67
+ "vspect",
68
+ ]
69
+
70
+ ## TOOLS
71
+
72
+ [tool.uv]
73
+ exclude-newer = "7 days"
74
+
75
+ [tool.ruff]
76
+ line-length = 99
77
+ src = ["src/ghtriage/**/*.py", "tests/**/*.py"]
78
+
79
+ [tool.ruff.lint]
80
+ select = [
81
+ "E", # Pyflakes
82
+ "F", # Pycodestyle
83
+ "I", # isort
84
+ ]
85
+ unfixable = ["F"]
86
+
87
+ [tool.ruff.lint.isort]
88
+ known-first-party = ["ghtriage"]
89
+ force-sort-within-sections = true
90
+
91
+ [tool.ty.src]
92
+ include = ["src"]
93
+
94
+ [tool.pytest.ini_options]
95
+ minversion = "6.0"
96
+ addopts = "--cov=ghtriage --cov-report=term --cov-report=html --cov-report=xml"
97
+ testpaths = ["tests"]
98
+
99
+ [tool.coverage.run]
100
+ source = ["ghtriage"]
101
+ # debug = ["pathmap"] # Useful if path mapping isn't working as expected
102
+
103
+ [tool.coverage.paths]
104
+ ghtriage = [
105
+ "src/ghtriage/",
106
+ "*/site-packages/ghtriage/"
107
+ ]
@@ -0,0 +1,5 @@
1
+ from ghtriage.cli import run
2
+
3
+
4
+ def main() -> None:
5
+ raise SystemExit(run())