whygit 0.0.0-stage → 0.1.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 whygit contributors
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.
package/README.md CHANGED
@@ -1,3 +1,276 @@
1
- # Temporary Holding Version
1
+ <div align="center">
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # whygit
4
+
5
+ **Git blame tells you *who*. whygit tells you *why*.**
6
+
7
+ Point it at any repository. It turns the reasons already buried in git history
8
+ into answers you and your coding agent can cite — file, line, commit, author, date.
9
+
10
+ [![npm](https://img.shields.io/npm/v/whygit?color=cb3837)](https://www.npmjs.com/package/whygit)
11
+ [![tests](https://github.com/carefreebit/whygit/actions/workflows/ci.yml/badge.svg)](https://github.com/carefreebit/whygit/actions/workflows/ci.yml)
12
+ [![node](https://img.shields.io/badge/node-%3E%3D22.5-3c873a)](https://nodejs.org)
13
+ [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
14
+ [![MCP](https://img.shields.io/badge/MCP-ready-6b46c1)](#give-it-to-your-agent)
15
+
16
+ [中文](README.zh-CN.md) · [Quickstart](#quickstart) · [Commands](#commands) · [Agents](#give-it-to-your-agent) · [How it works](#how-it-works)
17
+
18
+ </div>
19
+
20
+ ---
21
+
22
+ <p align="center">
23
+ <img src="docs/demo.svg" alt="whygit why &quot;cookie jar&quot; in a terminal" width="900">
24
+ </p>
25
+
26
+ <p align="center"><sub>Real output: <code>whygit why "cookie jar"</code> on <code>psf/requests</code> — 4,884 commits indexed in 3s, answered in 21ms.</sub></p>
27
+
28
+ That answer existed all along — in a commit body from 2015 that nobody has read since.
29
+ whygit found it in 21 milliseconds, without an LLM, an API key, or a network call.
30
+
31
+ ## The problem
32
+
33
+ Every codebase is full of lines that look wrong but are not. A retry that seems
34
+ redundant. A guard that seems paranoid. A `sleep(2)` that looks like laziness.
35
+
36
+ - `git blame` tells you **who** touched the line and **when**. Not why.
37
+ - `git log` is a write-only database. The reason is in the body of a commit from 2016.
38
+ - LLM summaries of history **invent plausible reasons**. In a codebase, a plausible
39
+ wrong reason is worse than no reason.
40
+ - Coding agents are the worst affected: they rewrite the guard, delete the retry, and
41
+ the bug comes back six months later — because nothing told them the line was load-bearing.
42
+
43
+ whygit reads what your repository already wrote down, extracts the sentences that
44
+ actually explain a decision, and makes them retrievable in the two places that matter:
45
+ **your terminal** and **your agent's context window**.
46
+
47
+ ## Quickstart
48
+
49
+ ```bash
50
+ npx whygit init # one pass over history, ~2s for 5k commits
51
+ npx whygit why "retry on 503" # natural-language question
52
+ npx whygit why src/client.js:42 # what is this exact line doing here?
53
+ npx whygit cursed # the files that keep coming back
54
+ npx whygit story src/client.js # one file, birth to HEAD
55
+ npx whygit whylog --write # write WHYLOG.md, the file agents read
56
+ ```
57
+
58
+ Requires **Node 22.5+** (for the built-in `node:sqlite`). Zero runtime dependencies.
59
+ No config file, no account, no telemetry.
60
+
61
+ The first command builds a local index at `.git/whygit/index.db`. Every command after
62
+ it is incremental: new commits are picked up automatically on the next run.
63
+
64
+ ## Commands
65
+
66
+ | Command | What it answers |
67
+ | --- | --- |
68
+ | `whygit why <words>` | Why does this behaviour exist? Ranked decisions with citations. |
69
+ | `whygit why <file>:<line>` | Why is *this line* still here? Blame + the decision behind it + how much of that change is still alive. |
70
+ | `whygit story <file>` | The biography of one file: birth, commits per year, the chapters that mattered, who kept it alive. |
71
+ | `whygit cursed` | Ranked risk map: churn × fix density × author spread × freshness. Read before refactoring. |
72
+ | `whygit whylog --write` | Generate `WHYLOG.md`: load-bearing decisions + cursed files, in Markdown. |
73
+ | `whygit check --base origin/main` | CI gate: what fraction of newly added lines trace to a commit that said why. |
74
+ | `whygit mcp` | MCP server over stdio for Claude Code, Cursor, Codex, and any MCP client. |
75
+ | `whygit install <agent>` | Writes the MCP config (`claude`, `cursor`, `vscode`, `agents`, `all`). |
76
+ | `whygit doctor` | Checks git, index freshness, shallow clones, missing WHYLOG. |
77
+
78
+ Every command takes `--json` for scripting, and `--cwd <path>` to point at another repo.
79
+
80
+ ### `why <file>:<line>`
81
+
82
+ ```console
83
+ $ whygit why src/requests/models.py:120
84
+
85
+ 1. basics working
86
+ 2012-12-15 · 366e8e84 · Kenneth Reitz · change
87
+ line 120 is 78 lines of this change, still in HEAD
88
+
89
+ 2. Add hasattr checks for remaining protocol isinstance checks (#7505)
90
+ 2026-06-09 · 6f66281a · change
91
+ ```
92
+
93
+ The line itself is unremarkable. What matters is that 78 lines around it are still
94
+ the original 2012 change — so the surrounding shape is load-bearing, not incidental.
95
+
96
+ ### `story <file>`
97
+
98
+ ```console
99
+ $ whygit story src/requests/models.py
100
+
101
+ born 2011-05-14 · package refactor
102
+
103
+ 2011 ██████████████████████████████████······ 217
104
+ 2012 ████████████████████████████████████████ 254
105
+ 2013 ██████████████·························· 86
106
+ ...
107
+
108
+ 717 commits · 202 authors · 154 fixes · 0 TODO markers
109
+ ─── the plot ──────────────────────────────────────────────────
110
+
111
+ 2015-04-06 Copy a PreparedRequest's CookieJar reliably
112
+ By adding _copy_cookie_jar we can reliably copy cookie jars so that we
113
+ have an actual copy instead of the same instance on different prepared
114
+ requests.
115
+ ```
116
+
117
+ ### `cursed`
118
+
119
+ ```console
120
+ $ whygit cursed
121
+
122
+ 1 src/requests/models.py ██████████████ 8.1 717 commits · 202 authors · 154 fixes
123
+ 2 src/requests/sessions.py █████████████· 7.5 334 commits · 110 authors · 79 fixes
124
+ 3 tests/test_requests.py ████████████·· 7.2 366 commits · 104 authors · 76 fixes
125
+ ```
126
+
127
+ Not a code-quality judgement — a map of where history is dense enough that a
128
+ confident rewrite is likely to be wrong.
129
+
130
+ ## Give it to your agent
131
+
132
+ An agent without history rewrites load-bearing code. An agent with history asks
133
+ better questions. One command wires it up:
134
+
135
+ ```bash
136
+ npx whygit install claude # .mcp.json
137
+ npx whygit install cursor # .cursor/mcp.json
138
+ npx whygit install vscode # .vscode/mcp.json
139
+ npx whygit install agents # appends a block to AGENTS.md (works with any agent)
140
+ npx whygit install all
141
+ ```
142
+
143
+ Add `--local` if `whygit` is on your `PATH` (otherwise the config uses `npx -y whygit@latest`).
144
+
145
+ The MCP server exposes five tools:
146
+
147
+ | Tool | Use it when |
148
+ | --- | --- |
149
+ | `why_search` | The agent is about to change behaviour it does not understand. |
150
+ | `why_line` | The agent is editing a specific line and needs the decision behind it. |
151
+ | `why_file` | The agent is refactoring a file and needs every decision that shaped it. |
152
+ | `why_risk` | The agent is choosing where to start, or where *not* to. |
153
+ | `whylog_read` | The agent wants the whole distilled history in one read. |
154
+
155
+ Add one line to your agent instructions, and the behaviour changes:
156
+
157
+ ```markdown
158
+ Before changing a line you do not understand, call `why_line` (MCP: whygit).
159
+ If a past decision contradicts your plan, say so before editing.
160
+ ```
161
+
162
+ ### WHYLOG.md — history your agent reads without being asked
163
+
164
+ `whygit whylog --write` produces a Markdown file that is small enough to live in a
165
+ repo and specific enough to change a decision:
166
+
167
+ - **Load-bearing decisions** — ranked, each with the verbatim reason, date, commit, author, and the files it touches.
168
+ - **The cursed files** — a table of where history is thickest.
169
+ - **House rule** — the one-line convention (`WHY: ...`) that makes future commits explain themselves.
170
+
171
+ It is generated from git, never hand-edited, and it diffs cleanly in review.
172
+ Commit it, and every agent that starts in the repo gets the context for free —
173
+ no MCP required.
174
+
175
+ ### CI gate
176
+
177
+ ```yaml
178
+ - run: npx whygit check --base origin/main --min 0.6
179
+ ```
180
+
181
+ Fails when fewer than 60% of newly added lines trace to a commit that stated a reason.
182
+ It is a nudge, not a linter: the fix is one sentence in the commit body.
183
+
184
+ ## How it works
185
+
186
+ ```text
187
+ git log (two passes: metadata + numstat)
188
+ │
189
+ ▼
190
+ signals: strip trailers, drop bot noise, de-duplicate squash-merge subjects,
191
+ keep sentences that contain a reason ("because", "so that", "to avoid",
192
+ "由于", "避免", "回滚" ...)
193
+ │
194
+ ▼
195
+ SQLite .git/whygit/index.db commits · files · decisions · FTS5 · risk
196
+ │
197
+ ▼
198
+ ranking: BM25 × confidence × churn × recency × fix-density × noise penalties
199
+ │
200
+ ├── whygit why / story / cursed (terminal)
201
+ ├── whygit whylog (WHYLOG.md)
202
+ └── whygit mcp (agents)
203
+ ```
204
+
205
+ **What a "decision" is:** a commit whose message contains a sentence explaining a
206
+ choice. whygit does not interpret that sentence — it quotes it. If the message says
207
+ nothing, the decision has low confidence and ranks below the ones that do.
208
+
209
+ **Why it can be fast:** the whole index is built from two `git log` passes, and
210
+ queries run against FTS5 in the same process. On a laptop, 4,884 commits index in
211
+ ~3 seconds and a query takes ~20 ms. No network, no model, no cache to warm.
212
+
213
+ **What it will not do:** invent a reason, summarise code, or judge quality. If the
214
+ history is silent, whygit says so — silence is a finding too.
215
+
216
+ ## Compared to
217
+
218
+ | | Answers "why" | Cites the source | Works offline | Agent-native |
219
+ | --- | :---: | :---: | :---: | :---: |
220
+ | `git blame` | no | — | yes | no |
221
+ | `git log` | only if you already know where to look | yes | yes | no |
222
+ | LLM-generated docs | sometimes | rarely | no | partly |
223
+ | **whygit** | **yes** | **yes (sha + file + line)** | **yes** | **yes (MCP + WHYLOG.md)** |
224
+
225
+ ## FAQ
226
+
227
+ **Does it send my code anywhere?** No. whygit reads local git data and writes one
228
+ SQLite file inside `.git/`. There is no network code path.
229
+
230
+ **Does it need an LLM?** No. That is the point: a cited sentence from 2016 beats a
231
+ confident paraphrase from a model.
232
+
233
+ **Does it work on a shallow clone?** It runs, and it tells you the history is
234
+ truncated (`whygit doctor`). Results are only as good as the history present.
235
+
236
+ **Squash merges?** Handled: the repeated subject line is removed and the PR body is
237
+ treated as the rationale.
238
+
239
+ **Monorepos?** Yes. Paths are indexed as-is; `why <file>:<line>` works from any depth.
240
+
241
+ **Does it modify my repository?** Only if you ask: `whylog --write`, `install`, and the
242
+ index under `.git/`. History is read-only.
243
+
244
+ ## Limitations
245
+
246
+ - Rationale can only be as good as commit messages. whygit makes silence visible; it cannot fill it.
247
+ - Very large histories (500k+ commits) index in minutes, not seconds.
248
+ - Renames are followed by path, not by content similarity: a file moved twice may have two histories.
249
+ - The ranking is heuristic. It is tuned to prefer cited reasons over churn, and it will still be wrong sometimes — each result shows its citations so you can check.
250
+
251
+ ## Contributing
252
+
253
+ ```bash
254
+ git clone https://github.com/carefreebit/whygit && cd whygit
255
+ npm test # 17 tests, no network
256
+ npm run lint
257
+ node bin/whygit.js why "..." --cwd /path/to/any/repo
258
+ ```
259
+
260
+ Issues and PRs welcome — especially real-world ranking failures. Paste the query, the
261
+ repo, and what should have ranked first.
262
+
263
+ ## License
264
+
265
+ MIT. See [LICENSE](LICENSE).
266
+
267
+ ---
268
+
269
+ <div align="center">
270
+
271
+ If whygit saved you from deleting a load-bearing line, a star helps the next person
272
+ find it.
273
+
274
+ **[⭐ Star whygit](https://github.com/carefreebit/whygit)**
275
+
276
+ </div>
@@ -0,0 +1,264 @@
1
+ <div align="center">
2
+
3
+ # whygit
4
+
5
+ **git blame 告诉你「谁」改的。whygit 告诉你「为什么」。**
6
+
7
+ 对着任意仓库跑一次,它把散落在 git 历史里的理由变成可引用的答案:
8
+ 文件、行号、commit、作者、日期,一个都不少。
9
+
10
+ [![npm](https://img.shields.io/npm/v/whygit?color=cb3837)](https://www.npmjs.com/package/whygit)
11
+ [![tests](https://github.com/carefreebit/whygit/actions/workflows/ci.yml/badge.svg)](https://github.com/carefreebit/whygit/actions/workflows/ci.yml)
12
+ [![node](https://img.shields.io/badge/node-%3E%3D22.5-3c873a)](https://nodejs.org)
13
+ [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
14
+ [![MCP](https://img.shields.io/badge/MCP-ready-6b46c1)](#交给你的-agent)
15
+
16
+ [English](README.md) · [快速开始](#快速开始) · [命令](#命令) · [Agent 接入](#交给你的-agent) · [原理](#它是怎么做到的)
17
+
18
+ </div>
19
+
20
+ ---
21
+
22
+ <p align="center">
23
+ <img src="docs/demo.svg" alt="whygit why &quot;cookie jar&quot; 终端演示" width="900">
24
+ </p>
25
+
26
+ <p align="center"><sub>真实输出:在 <code>psf/requests</code> 上执行 <code>whygit why "cookie jar"</code> —— 4,884 个 commit 索引 3 秒,回答 21 毫秒。</sub></p>
27
+
28
+ 这个答案一直躺在仓库里——2015 年某次 commit 的正文,此后九年没人读过。
29
+ whygit 用 21 毫秒把它找出来:没有大模型,没有 API key,没有网络请求。
30
+
31
+ ## 它解决什么问题
32
+
33
+ 每个代码库里都有一批「看起来很蠢、其实很关键」的代码:
34
+ 一次显得多余的重试、一个看起来神经质的判空、一句像偷懒的 `sleep(2)`。
35
+
36
+ - `git blame` 只回答**谁**、**什么时候**动了这一行,不回答为什么;
37
+ - `git log` 是个只写不读的数据库,理由埋在 2016 年某条 commit 正文里;
38
+ - 让大模型去总结 git 历史,它会**编出看起来合理的理由**。在代码库里,一个听起来对的错理由,比没有理由更危险;
39
+ - 编码 Agent 受害最深:它们删掉重试、改写判空,半年后同一个 bug 回来——因为没有任何东西告诉它「这行是承重的」。
40
+
41
+ whygit 只做一件事:把仓库**已经写下的**理由读出来,提取真正在解释决策的句子,
42
+ 送到两个真正有用的地方——**你的终端**,和 **Agent 的上下文窗口**。
43
+
44
+ ## 快速开始
45
+
46
+ ```bash
47
+ npx whygit init # 扫一遍历史,5k commit 约 2 秒
48
+ npx whygit why "retry on 503" # 用自然语言问
49
+ npx whygit why src/client.js:42 # 这一行到底为什么在这里
50
+ npx whygit cursed # 反复出问题的文件排行
51
+ npx whygit story src/client.js # 单个文件从诞生到 HEAD 的一生
52
+ npx whygit whylog --write # 生成 WHYLOG.md,给 Agent 读的历史笔记
53
+ ```
54
+
55
+ 需要 **Node 22.5+**(用到内置的 `node:sqlite`)。零运行时依赖,无配置文件,
56
+ 无账号,无遥测。
57
+
58
+ 第一次运行会在 `.git/whygit/index.db` 建本地索引;之后每次都是增量更新,
59
+ 新 commit 会被自动补进来。
60
+
61
+ ## 命令
62
+
63
+ | 命令 | 回答什么 |
64
+ | --- | --- |
65
+ | `whygit why <关键词>` | 这个行为为什么存在?按相关度排序,每条都带出处。 |
66
+ | `whygit why <文件>:<行号>` | 这一行为什么还活着?blame + 背后的决策 + 这次改动还剩多少行在 HEAD。 |
67
+ | `whygit story <文件>` | 一个文件的传记:出生、按年提交量、关键章节、谁一直在维护它。 |
68
+ | `whygit cursed` | 风险地图:改动量 × 修复密度 × 作者分散度 × 新鲜度。重构前先看。 |
69
+ | `whygit whylog --write` | 生成 `WHYLOG.md`:承重决策 + 高危文件,纯 Markdown。 |
70
+ | `whygit check --base origin/main` | CI 门禁:新增代码行里,有多大比例能追溯到「说过为什么」的 commit。 |
71
+ | `whygit mcp` | 基于 stdio 的 MCP 服务,Claude Code、Cursor、Codex 及任何 MCP 客户端可用。 |
72
+ | `whygit install <agent>` | 写入 MCP 配置(`claude` / `cursor` / `vscode` / `agents` / `all`)。 |
73
+ | `whygit doctor` | 自检:git、索引是否过期、是否浅克隆、WHYLOG 是否缺失。 |
74
+
75
+ 所有命令都支持 `--json`(给脚本用)和 `--cwd <path>`(指向别的仓库)。
76
+
77
+ ### `why <文件>:<行号>`
78
+
79
+ ```console
80
+ $ whygit why src/requests/models.py:120
81
+
82
+ 1. basics working
83
+ 2012-12-15 · 366e8e84 · Kenneth Reitz · change
84
+ line 120 is 78 lines of this change, still in HEAD
85
+
86
+ 2. Add hasattr checks for remaining protocol isinstance checks (#7505)
87
+ 2026-06-09 · 6f66281a · change
88
+ ```
89
+
90
+ 这一行本身平平无奇。真正重要的是:它周围 78 行仍然来自 2012 年那次改动——
91
+ 也就是说,这里的结构是承重的,不是随手写的。
92
+
93
+ ### `story <文件>`
94
+
95
+ ```console
96
+ $ whygit story src/requests/models.py
97
+
98
+ born 2011-05-14 · package refactor
99
+
100
+ 2011 ██████████████████████████████████······ 217
101
+ 2012 ████████████████████████████████████████ 254
102
+ 2013 ██████████████·························· 86
103
+ ...
104
+
105
+ 717 commits · 202 authors · 154 fixes · 0 TODO markers
106
+ ─── the plot ──────────────────────────────────────────────────
107
+
108
+ 2015-04-06 Copy a PreparedRequest's CookieJar reliably
109
+ By adding _copy_cookie_jar we can reliably copy cookie jars so that we
110
+ have an actual copy instead of the same instance on different prepared
111
+ requests.
112
+ ```
113
+
114
+ ### `cursed`
115
+
116
+ ```console
117
+ $ whygit cursed
118
+
119
+ 1 src/requests/models.py ██████████████ 8.1 717 commits · 202 authors · 154 fixes
120
+ 2 src/requests/sessions.py █████████████· 7.5 334 commits · 110 authors · 79 fixes
121
+ 3 tests/test_requests.py ████████████·· 7.2 366 commits · 104 authors · 76 fixes
122
+ ```
123
+
124
+ 这不是代码质量评分,而是一张地图:标出历史密度最高的地方——在那里自信地重写,
125
+ 大概率是错的。
126
+
127
+ ## 交给你的 Agent
128
+
129
+ 不知道历史的 Agent 会重写承重代码;知道历史的 Agent 会先问对问题。一条命令接好:
130
+
131
+ ```bash
132
+ npx whygit install claude # .mcp.json
133
+ npx whygit install cursor # .cursor/mcp.json
134
+ npx whygit install vscode # .vscode/mcp.json
135
+ npx whygit install agents # 往 AGENTS.md 追加一段(任何 Agent 都认)
136
+ npx whygit install all
137
+ ```
138
+
139
+ 如果 `whygit` 已在 `PATH` 里,加 `--local`(否则配置里用 `npx -y whygit@latest`)。
140
+
141
+ MCP 服务暴露五个工具:
142
+
143
+ | 工具 | 什么时候用 |
144
+ | --- | --- |
145
+ | `why_search` | Agent 即将改动一段它并不理解的行为。 |
146
+ | `why_line` | Agent 正在编辑某一行,需要这行背后的决策。 |
147
+ | `why_file` | Agent 要重构一个文件,需要塑造过它的全部决策。 |
148
+ | `why_risk` | Agent 在挑起点,或者在挑「别碰哪里」。 |
149
+ | `whylog_read` | Agent 想一口气读完蒸馏后的历史。 |
150
+
151
+ 在 Agent 的指令文件里加一行,行为就变了:
152
+
153
+ ```markdown
154
+ 改动任何你不理解的行之前,先调用 `why_line`(MCP:whygit)。
155
+ 如果历史决策和你的方案冲突,先说出来,再动手。
156
+ ```
157
+
158
+ ### WHYLOG.md —— Agent 不用被提醒就会读的历史
159
+
160
+ `whygit whylog --write` 生成一份 Markdown:小到可以进仓库,具体到能改变一次决策。
161
+
162
+ - **承重决策**:按重要性排序,每条都带原文理由、日期、commit、作者、涉及文件;
163
+ - **高危文件**:历史最稠密的地方;
164
+ - **仓库规矩**:一行约定(`WHY: ...`),让以后的 commit 自己解释自己。
165
+
166
+ 它由 git 生成、永不手改、在 review 里 diff 干净。提交进仓库后,
167
+ 每个在这个仓库里启动的 Agent 都免费获得上下文——**不需要 MCP**。
168
+
169
+ ### CI 门禁
170
+
171
+ ```yaml
172
+ - run: npx whygit check --base origin/main --min 0.6
173
+ ```
174
+
175
+ 当新增代码行中能追溯到「说过理由」的 commit 的比例低于 60% 时失败。
176
+ 它不是 linter,只是提醒:修复方式是在 commit 正文里写一句话。
177
+
178
+ ## 它是怎么做到的
179
+
180
+ ```text
181
+ git log(两遍:元数据 + numstat)
182
+ │
183
+ ▼
184
+ 信号提取:去掉 trailer、过滤机器人噪音、去重 squash-merge 重复标题,
185
+ 保留含因果词的句子(because / so that / to avoid / 由于 / 避免 / 回滚 ...)
186
+ │
187
+ ▼
188
+ SQLite .git/whygit/index.db commits · files · decisions · FTS5 · risk
189
+ │
190
+ ▼
191
+ 排序:BM25 × 置信度 × 改动量 × 新鲜度 × 修复密度 × 噪音惩罚
192
+ │
193
+ ├── whygit why / story / cursed (终端)
194
+ ├── whygit whylog (WHYLOG.md)
195
+ └── whygit mcp (Agent)
196
+ ```
197
+
198
+ **什么叫一条「决策」**:commit message 里存在解释选择的句子。
199
+ whygit 不解释这句话,只引用它。如果 message 什么都没说,这条决策置信度就低,
200
+ 排在说过理由的后面。
201
+
202
+ **为什么快**:整个索引只来自两遍 `git log`,查询在同一个进程里打 FTS5。
203
+ 在普通笔记本上,4,884 个 commit 建索引约 3 秒,单次查询约 20 毫秒。
204
+ 没有网络、没有模型、没有缓存要预热。
205
+
206
+ **它不会做什么**:编理由、总结代码、评判质量。历史沉默时,whygit 就说沉默——
207
+ 沉默本身也是一种结论。
208
+
209
+ ## 和其他方案比
210
+
211
+ | | 回答「为什么」 | 给出处 | 可离线 | 为 Agent 而生 |
212
+ | --- | :---: | :---: | :---: | :---: |
213
+ | `git blame` | 否 | — | 是 | 否 |
214
+ | `git log` | 只有你已经知道去哪找时 | 是 | 是 | 否 |
215
+ | 大模型生成的文档 | 有时 | 很少 | 否 | 部分 |
216
+ | **whygit** | **是** | **是(sha + 文件 + 行)** | **是** | **是(MCP + WHYLOG.md)** |
217
+
218
+ ## 常见问题
219
+
220
+ **会把我的代码传到网上吗?** 不会。whygit 只读本地 git 数据,只往 `.git/` 里写一个 SQLite 文件,
221
+ 代码里没有任何网络调用路径。
222
+
223
+ **需要大模型吗?** 不需要,这正是重点:一句 2016 年带出处的原话,胜过模型的自信转述。
224
+
225
+ **浅克隆能用吗?** 能跑,但会明确告诉你历史被截断了(`whygit doctor`)。结果只和现存历史一样好。
226
+
227
+ **squash merge 呢?** 已处理:去掉正文里重复的标题行,把 PR 描述当作理由。
228
+
229
+ **monorepo?** 支持。路径按原样索引,`why <文件>:<行号>` 在任意层级都可用。
230
+
231
+ **会改我的仓库吗?** 只在你明确要求时:`whylog --write`、`install`,以及 `.git/` 下的索引。
232
+ 历史始终只读。
233
+
234
+ ## 局限
235
+
236
+ - 理由的上限就是 commit message 的质量。whygit 让沉默可见,但没法替你补上;
237
+ - 超大历史(50 万+ commit)建索引是分钟级,不是秒级;
238
+ - 重命名按路径追踪,不做内容相似度匹配:一个文件搬过两次,可能有两段历史;
239
+ - 排序是启发式的,偏向「有出处的理由」而不是改动量,仍会出错——每条结果都带引用,方便你自己核对。
240
+
241
+ ## 参与贡献
242
+
243
+ ```bash
244
+ git clone https://github.com/carefreebit/whygit && cd whygit
245
+ npm test # 17 个测试,不联网
246
+ npm run lint
247
+ node bin/whygit.js why "..." --cwd /path/to/any/repo
248
+ ```
249
+
250
+ 欢迎 issue 和 PR——尤其是真实的排序失败案例。请附上查询、仓库,以及你认为本应排第一的结果。
251
+
252
+ ## 许可证
253
+
254
+ MIT,见 [LICENSE](LICENSE)。
255
+
256
+ ---
257
+
258
+ <div align="center">
259
+
260
+ 如果 whygit 帮你拦下了一次「删掉承重代码」的操作,点个 star,能让下一个人找到它。
261
+
262
+ **[⭐ Star whygit](https://github.com/carefreebit/whygit)**
263
+
264
+ </div>
package/bin/whygit.js ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import process from 'node:process';
3
+ import { main } from '../src/cli.js';
4
+
5
+ main(process.argv.slice(2))
6
+ .then(function (code) { process.exitCode = code || 0; })
7
+ .catch(function (err) {
8
+ process.stderr.write('whygit: ' + (err && err.message ? err.message : String(err)) + '\n');
9
+ if (process.env.WHYGIT_DEBUG) console.error(err);
10
+ process.exitCode = 1;
11
+ });
package/docs/demo.svg ADDED
@@ -0,0 +1,21 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="900" height="570" viewBox="0 0 900 570" role="img" aria-label="whygit terminal demo">
2
+ <defs><linearGradient id="bg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#161b22"/><stop offset="1" stop-color="#0d1117"/></linearGradient></defs>
3
+ <rect width="900" height="570" rx="14" fill="url(#bg)" stroke="#30363d"/>
4
+ <rect width="900" height="42" rx="14" fill="#21262d"/><rect y="28" width="900" height="14" fill="#21262d"/>
5
+ <circle cx="24" cy="21" r="6" fill="#ff5f56"/><circle cx="46" cy="21" r="6" fill="#ffbd2e"/><circle cx="68" cy="21" r="6" fill="#27c93f"/>
6
+ <text x="450" y="26" text-anchor="middle" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="12" fill="#8b949e">whygit — psf/requests</text>
7
+ <text x="30" y="74" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13.5" fill="#7ee787" font-weight="600" xml:space="preserve">$ whygit why "cookie jar"</text>
8
+ <text x="30" y="116" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13.5" fill="#e6edf3" font-weight="600" xml:space="preserve"> whygit psf/requests 4,884 commits</text>
9
+ <text x="30" y="137" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13.5" fill="#30363d" xml:space="preserve"> ────────────────────────────────────</text>
10
+ <text x="30" y="200" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13.5" fill="#f0f6fc" font-weight="600" xml:space="preserve"> 1. Copy a PreparedRequest's CookieJar reliably</text>
11
+ <text x="30" y="221" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13.5" fill="#8b949e" xml:space="preserve"> 2015-04-06 · 36093e69 · Ian Cordasco · change · 100% match</text>
12
+ <text x="30" y="263" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13.5" fill="#c9d1d9" xml:space="preserve"> By adding _copy_cookie_jar we can reliably copy cookie jars so that we have an</text>
13
+ <text x="30" y="284" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13.5" fill="#c9d1d9" xml:space="preserve"> actual copy instead of the same instance on different prepared requests.</text>
14
+ <text x="30" y="326" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13.5" fill="#79c0ff" xml:space="preserve"> requests/cookies.py requests/models.py</text>
15
+ <text x="30" y="347" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13.5" fill="#58a6ff" text-decoration="underline" xml:space="preserve"> → https://github.com/psf/requests/commit/36093e69c70b9b24dd1e007befa35ad891313f51</text>
16
+ <text x="30" y="389" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13.5" fill="#f0f6fc" font-weight="600" xml:space="preserve"> 2. Test and perfection for cookie handling.</text>
17
+ <text x="30" y="410" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13.5" fill="#8b949e" xml:space="preserve"> 2013-02-11 · 2e316961 · Ian Cordasco · fix · 71% match</text>
18
+ <text x="30" y="452" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13.5" fill="#c9d1d9" xml:space="preserve"> I also fixed up some of the RequestsCookieJar methods so using</text>
19
+ <text x="30" y="473" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13.5" fill="#c9d1d9" xml:space="preserve"> jar.update(other_jar) works without a problem.</text>
20
+ <text x="30" y="515" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13.5" fill="#8b949e" xml:space="preserve"> 2 of 15 decisions · 21ms · whygit why "cookie jar"</text>
21
+ </svg>
package/package.json CHANGED
@@ -1,6 +1,52 @@
1
1
  {
2
2
  "name": "whygit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Git blame tells you who. whygit tells you why. Turn git history into cited, agent-readable context.",
5
+ "type": "module",
6
+ "bin": {
7
+ "whygit": "bin/whygit.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=22.5.0"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "src",
15
+ "docs",
16
+ "README.md",
17
+ "README.zh-CN.md",
18
+ "LICENSE"
19
+ ],
20
+ "scripts": {
21
+ "test": "node --test test/signals.test.js test/index.test.js",
22
+ "lint": "node scripts/lint.js"
23
+ },
24
+ "keywords": [
25
+ "git",
26
+ "blame",
27
+ "archaeology",
28
+ "why",
29
+ "mcp",
30
+ "ai-agents",
31
+ "claude-code",
32
+ "codex",
33
+ "cursor",
34
+ "context",
35
+ "knowledge-base",
36
+ "code-review",
37
+ "onboarding"
38
+ ],
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/carefreebit/whygit.git"
43
+ },
44
+ "homepage": "https://github.com/carefreebit/whygit#readme",
45
+ "bugs": {
46
+ "url": "https://github.com/carefreebit/whygit/issues"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "author": "lxh"
52
+ }